Repo Proxy
Repo Proxy is the subsystem that brings external element repositories into Exordos Core. It discovers elements from remote sources, stores them locally, resolves dependencies between elements, and bridges installed repo elements to the Elements Management (EM) layer so they can be deployed on compute nodes.
Overview#
A Repository represents an external source of elements. Each repository is backed by a driver that knows how to fetch the inventory and download individual elements. Elements are stored as RepoElements with their manifests, specifications, and artifacts. Users can install, upgrade, and uninstall repo elements through the User API. Installation triggers the universal-agent builder pipeline, which creates an EM Manifest and links it to an EM Element.
The subsystem runs as part of the exordos-repo-proxy-gservice launchpad
process, which hosts three services:
- RepoProxyBuilderService — manages repository lifecycle (creation, refresh, inventory sync).
- RepoElementBuilderService — manages repo element lifecycle (install, upgrade, uninstall, dependency resolution).
- RepoElementAgentService — universal agent that translates
repo_proxy_installed_elementresources into EM manifests and elements.
Data Models#
All models are defined in exordos_core/repo/dm/models.py and persisted in
PostgreSQL via restalchemy ORM.
Repository#
Table: repo_repositories
| Field | Type | Description |
|---|---|---|
uuid |
UUID PK | Unique identifier |
name |
String(255) | Repository name (unique per project) |
description |
Text | Free-form description |
project_id |
UUID | Owning project |
status |
Enum | NEW, ACTIVE, IN_PROGRESS, DISABLED, ERROR |
priority |
Integer (0–16384) | Higher = preferred for dependency resolution. Default: 8192 |
refresh_rate |
Integer (seconds) | How often to re-fetch inventory. 0 = disabled. Default: 3600 |
sync_mode |
Enum | copy — download full element data immediately; lazy — download only name/version, fetch manifest on demand |
driver_spec |
KindModel (JSONB) | Driver configuration. One of NginxDriverSpec, BootstrapDriverSpec, DummyMigrationDriverSpec |
next_refresh |
DateTime | When the next periodic refresh should occur |
Key methods:
load_driver()— discovers and instantiates the correct driver by iterating entry points in theexordos.repo_proxy.driversgroup. Each driver checksdriver_spec.KINDand raisesValueErrorif it does not match. The first successful driver is cached in__driver_map__.iter_elements_in_inventory()— fetches the inventory via the driver and yieldsRepoElementinstances one by one (generator).actualize_element(element)— downloads the full element data (manifest, specification, inventory) and replaces artifacts in the database.refresh()— setsnext_refreshto now, triggering an early refresh.upload(name, version, manifest)— uploads an element to the repository if the driver supports it.
RepoElement#
Table: repo_elements
| Field | Type | Description |
|---|---|---|
uuid |
UUID PK | Unique identifier |
name |
String(255) | Element name |
version |
String(255) | Semantic version string |
description |
Text | Free-form description |
project_id |
UUID | Owning project |
repository |
FK → Repository | Parent repository |
status |
Enum | NEW, AVAILABLE, ACTIVE, IN_PROGRESS, ERROR |
installation_state |
Enum | INSTALLED or UNINSTALLED |
manifest |
JSONB | Full manifest dict (resources, requirements, exports, imports, etc.) |
specification |
JSONB | Specification dict |
inventory |
JSONB | Inventory dict (artifact index, configs, images, manifests) |
element |
UUID → em_elements |
Linked EM Element UUID (set after installation) |
Unique constraint: (repository, name, version).
Key methods:
dependencies(property) — readsmanifest["requirements"]and converts constraint keys:from_version→>=,to_version→<,version→==.install()— marks the element asINSTALLED. Rejects if another element with the same name is already installed.uninstall()— marks the element asUNINSTALLED. Rejects if other installed elements depend on it (checked viaRepoElementDepsBinding).upgrade(target)— swaps installation state from the current element to a target element (same name, different version). The runtime EM Element UUID is transferred.edit(manifest)— replaces the manifest dict. Validates thatnameandversionin the new manifest match the element's fields.from_inventory(repository, inventory)(classmethod) — creates aRepoElementand associatedRepoArtifactinstances from an inventory dict.
RepoArtifact#
Table: repo_artifacts
| Field | Type | Description |
|---|---|---|
uuid |
UUID PK | Unique identifier |
project_id |
UUID | Owning project |
element |
FK → RepoElement | Parent element |
urn |
String(2048) | Uniform Resource Name (category + key) |
uri |
String(2048) | Full URL for artifact access |
Unique constraint: (element, urn).
RepoElementDepsBinding#
Table: repo_element_deps_bindings
| Field | Type | Description |
|---|---|---|
uuid |
UUID PK | Unique identifier |
element |
FK → RepoElement | The element that has dependencies |
depends_on |
FK → RepoElement | The element that is depended upon |
Unique constraint: (element, depends_on).
This table records transitive dependency relationships. When element C
depends on B and B depends on A, two records are created for C: (C, B) and
(C, A). The sole purpose is to prevent uninstallation of an element that sits
in the middle of a dependency chain — at uninstall time, the system checks
whether any depends_on record references the element being removed.
Driver Specs#
Driver specs are discriminated unions (kind models) stored in the
driver_spec JSONB column. Each spec has a KIND string that determines which
driver class handles it.
NginxDriverSpec (kind: "nginx")#
For remote HTTP repositories served by Nginx (or any static file server).
| Field | Type | Description |
|---|---|---|
url |
String(2048) | Base URL of the repository |
username |
String(255) | Optional basic-auth username |
password |
String(255) | Optional basic-auth password |
The inventory file is fetched from {url}/inventory.json. Individual elements
are fetched from {url}/{name}/{version}/manifests/{name}.yaml and
{url}/{name}/{version}/inventory.json.
BootstrapDriverSpec (kind: "bootstrap")#
For local filesystem repositories used during bootstrap. Reads YAML manifests from a local directory.
| Field | Type | Description |
|---|---|---|
manifests_dir |
String(2048) | Path to directory containing manifest YAML files |
A file is considered a valid manifest if it has a .yaml/.yml extension, can
be loaded as YAML, is a dict, and contains name, version, and resources
keys.
DummyMigrationDriverSpec (kind: "dummy_migration")#
A stub driver that exists solely so that RepoElement records can be attached
to a Repository during data migration from older installations that did not
have the repo proxy subsystem. It does not provide any real repository
functionality — get_inventory() returns an empty elements dict, and
get_element() reads from the database directly.
Drivers#
All drivers extend AbstractProxyRepoDriver
(exordos_core/repo/drivers/base.py) and are registered via the
exordos.repo_proxy.drivers entry point group in pyproject.toml:
[project.entry-points."exordos.repo_proxy.drivers"]
NginxProxyRepoDriver = "exordos_core.repo.drivers.nginx:NginxProxyRepoDriver"
BootstrapProxyRepoDriver = "exordos_core.repo.drivers.bootstrap:BootstrapProxyRepoDriver"
DummyMigrationRepoDriver = "exordos_core.repo.drivers.dummy_migration:DummyMigrationRepoDriver"
Abstract interface#
class AbstractProxyRepoDriver(abc.ABC):
def __init__(self, repository: Repository) -> None: ...
@property
@abc.abstractmethod
def repo_uri(self) -> str: ...
@abc.abstractmethod
def artifact_uri(self, element_name, element_version, category, artifact_name) -> str: ...
@abc.abstractmethod
def get_inventory(self) -> dict: ...
@abc.abstractmethod
def get_element(self, name, version="latest") -> tuple[RepoElement, Collection[RepoArtifact]]: ...
def can_upload_element(self, name, version) -> bool: ...
def upload_element(self, element: RepoElement) -> None: ...
Adding a new driver#
- Create a new driver spec model in
exordos_core/repo/dm/models.pythat extendstypes_dynamic.AbstractKindModelwith a uniqueKINDstring. - Add the spec to the
KindModelSelectorTypeinRepository.driver_spec. - Implement the driver class in
exordos_core/repo/drivers/, extendingAbstractProxyRepoDriver. The constructor must raiseValueErrorifrepository.driver_spec.KINDdoes not match. - Register the driver in
pyproject.tomlunder[project.entry-points."exordos.repo_proxy.drivers"]. - Create a database migration if the spec introduces new fields that need
schema changes (the
driver_speccolumn is JSONB, so no migration is needed for new spec kinds).
Services#
RepoProxyBuilderService#
File: exordos_core/repo/builders/repository.py
Manages the repository lifecycle as a universal-agent builder service. The
model class Repository (in the builders module) extends models.Repository
with InstanceMixin, registering the resource kind
repo_proxy_repository.
Key behaviours:
post_create_instance_resource— after a new repository is created, iterates the inventory and saves elements. Incopymode, each element is actualized (full data downloaded). Inlazymode, only name/version are saved. Sets status toACTIVEand schedulesnext_refresh._check_refresh— runs every 10 iterations (REFRESH_CHECK_ITERATION_INTERVAL). For each repository wherenext_refreshhas passed, callsrepo.update(force=True)to trigger the update hook._refresh_repository— compares existing elements against the current inventory. Adds new elements, removes stale ones (but preserves installed elements). Updatesnext_refresh.post_update_instance_resource— ifnext_refreshhas passed, calls_refresh_repository.
RepoElementBuilderService#
File: exordos_core/repo/builders/element.py
Manages the repo element lifecycle. The model class RepoElement (in the
builders module) extends models.RepoElement with
InstanceWithDerivativesMixin, registering the resource kind
repo_proxy_element and the derivative model InstalledManifest (kind
repo_proxy_installed_element).
InstalledManifest#
A derivative model that represents an installed repo element in the universal
agent layer. It carries the manifest payload, version, status, and the UUID of
the runtime EM Element (element field). Created via
InstalledManifest.from_repo_element(element).
Lifecycle detection#
The service determines what action to take based on the combination of
installation_state and element:
installation_state |
element |
Action |
|---|---|---|
INSTALLED |
None |
Install — element needs to be installed |
INSTALLED |
set | Upgrade — element is installed and linked, may need update |
UNINSTALLED |
set | Release — element was installed, now being uninstalled |
UNINSTALLED |
None |
Delete — ordinary deletion |
Dependency resolution#
_collect_dependencies(instance) performs a recursive depth-first traversal of
the element's dependency graph:
- If the element's manifest is empty (lazy mode), actualizes it first.
- For each dependency (from
manifest["requirements"]), queries allRepoElementrecords matching the dependency name. - Checks if any installed element violates the version constraint
(
DependencyConstraintError). - Filters candidates that satisfy the constraint
(
_matches_version_constraint). - If no candidates match, raises
DependencyNotFoundError. - Selects the best candidate using
_element_sort_key— preferring installed elements, higher repository priority, release versions, and higher version numbers. - Recurses into the selected dependency.
- Returns a flat list in install order (dependencies first, element last).
Version comparison supports ==, >, >=, <, <= operators. Release
versions (no suffix) are considered greater than development versions with the
same major.minor.patch base.
Installation flow#
_install_manifest(instance):
- Collects the full dependency closure.
- Installs each dependency in order (skips already-installed ones).
- Records
RepoElementDepsBindingentries for all transitive dependencies. - Sets element status to
IN_PROGRESS. - Returns an
InstalledManifestderivative, which the builder pushes to the universal agent.
Upgrade flow#
Upgrades are handled carefully to avoid gaps in EM:
- The old
InstalledManifestis not deleted before the new one is created. can_update_instance_resourcechecks that the pending (new) element has anInstalledManifestrecord with a linkedelementUUID before allowing the old one to be released.update_instance_derivativesreturns the new derivative for installation, then on the next iteration returns an empty collection for the old element, triggering its deletion.
RepoElementAgentService#
File: exordos_core/repo/agents/universal/service.py
A universal agent service that exposes the repo_proxy_installed_element
capability to the orchestrator. It uses RepoElementCapabilityDriver as its
capability driver.
RepoElementCapabilityDriver and RepoEmBackendClient#
File: exordos_core/repo/agents/universal/drivers/repo_element.py
The capability driver wires a RepoEmBackendClient that translates universal
agent resource operations into EM lifecycle calls:
_create— converts the resource to an EMManifest, saves it, and callsmanifest.install(). If the element is already installed, callsmanifest.upgrade()instead. Stores the resulting EM Element UUID._update— updates the EM Manifest fields and triggersmanifest.upgrade()if resource fields changed._delete— callsmanifest.uninstall()thenmanifest.delete()._list— joins EM Manifests with EM Elements by(name, version)and returnsInstalledManifestinstances.
Target fields are stored locally in a JSON file
(repo_elements_target_fields.json) inside the agent work directory.
API#
The User API is served under /v1/repo/ and defined in
exordos_core/user_api/repo/api/.
Endpoints#
| Method | Path | Description |
|---|---|---|
| GET/FILTER | /v1/repo/repositories |
List/filter repositories |
| POST | /v1/repo/repositories |
Create a repository |
| GET | /v1/repo/repositories/{uuid} |
Get a repository |
| PUT | /v1/repo/repositories/{uuid} |
Update a repository |
| DELETE | /v1/repo/repositories/{uuid} |
Delete a repository |
| POST | /v1/repo/repositories/{uuid}/actions/refresh/invoke |
Force refresh |
| POST | /v1/repo/repositories/{uuid}/actions/upload/invoke |
Upload element |
| GET/FILTER | /v1/repo/elements |
List/filter repo elements |
| GET | /v1/repo/elements/{uuid} |
Get element (actualizes if lazy) |
| DELETE | /v1/repo/elements/{uuid} |
Delete element |
| POST | /v1/repo/elements/{uuid}/actions/install/invoke |
Install element |
| POST | /v1/repo/elements/{uuid}/actions/uninstall/invoke |
Uninstall element |
| POST | /v1/repo/elements/{uuid}/actions/upgrade/invoke |
Upgrade element |
| POST | /v1/repo/elements/{uuid}/actions/edit/invoke |
Edit manifest |
Field permissions#
- Repository:
status,created_at,updated_atare read-only.next_refreshis hidden from API responses. - RepoElement:
status,created_at,updated_atare read-only.installation_stateis hidden from API responses.
IAM#
Both controllers use PolicyBasedController with policy service name repo.
Repository resources use policy name repository; element resources use
policy name element.
Inventory Format#
The inventory is a JSON document served by the repository driver. It lists all available elements:
{
"elements": {
"core": {
"0.1.12-dev+abc123": {
"name": "core",
"version": "0.1.12-dev+abc123",
"artifacts": ["openapi_user.yaml"],
"configs": [],
"images": ["exordos-core.qcow2"],
"manifests": ["core.yaml"],
"templates": [],
"index": {
"artifacts": {"<uuid>": "openapi_user.yaml"},
"configs": {},
"images": {"<uuid>": "exordos-core.qcow2"},
"manifests": {"<uuid>": "core.yaml"},
"templates": {}
}
}
}
}
}
Each element version entry contains:
artifacts,configs,images,manifests,templates— lists of file names by category.index— maps each category to a dict of{uuid: filename}entries. The uuid is used to construct the artifact URN as{category}:{uuid}.
Exceptions#
Defined in exordos_core/repo/exceptions.py:
DependencyNotFoundError— no suitable dependency element found matching the name and version constraint.DependencyConstraintError— an installed dependency exists but its version does not satisfy the required constraint.DependencyConstraintFormatError— the constraint dict contains unsupported operators.
Configuration#
Launchpad (exordos_core.conf.j2)#
[launchpad]
common_registrator_opts = exordos_core.cmd.repo_proxy_gservice:register_common_opts
common_initializer = exordos_core.cmd.repo_proxy_gservice:init_common_conf
services =
exordos_core.repo.builders.repository:RepoProxyBuilderService,
exordos_core.repo.builders.element:RepoElementBuilderService,
exordos_core.repo.agents.universal.service:RepoElementAgentService
Universal Agent (exordos_universal_agent.conf.j2)#
The repo_proxy_installed_element capability must be listed in the scheduler:
[universal_agent_scheduler]
capabilities =
em_*,
password,
certificate,
paas_lb_agent,
repo_proxy_installed_element
Architecture Diagram#
graph TB
subgraph "User API"
RC[RepositoryController]
EC[RepoElementController]
end
subgraph "Repo Proxy Service (launchpad)"
RBS[RepoProxyBuilderService]
EBS[RepoElementBuilderService]
RAS[RepoElementAgentService]
end
subgraph "Database"
RR[repo_repositories]
RE[repo_elements]
RA[repo_artifacts]
RDB[repo_element_deps_bindings]
end
subgraph "Elements Management"
EM_M[em_manifests]
EM_E[em_elements]
end
subgraph "External"
DRV[Driver<br/>Nginx / Bootstrap / Dummy]
EXT[External Repository]
end
RC --> RR
EC --> RE
RBS --> RR
RBS --> DRV
DRV --> EXT
RBS --> RE
EBS --> RE
EBS --> RDB
EBS -->|InstalledManifest| RAS
RAS -->|create/update/delete| EM_M
RAS -->|install/upgrade| EM_E
EM_M --> EM_E
RE -->|element UUID| EM_E
Key Design Decisions#
- Lazy vs Copy sync modes: Lazy mode stores only element name/version at discovery time, deferring the full manifest download until the element is actually accessed (via API GET or during installation). This reduces initial load for large repositories. Copy mode downloads everything upfront.
- Driver discovery via entry points: Drivers are discovered at runtime via
Python entry points, allowing third-party drivers to be registered without
modifying core code. The
load_driver()method tries each registered driver and uses the first one that accepts thedriver_speckind. - Dependency bindings for uninstall safety: The
RepoElementDepsBindingtable exists solely to prevent uninstallation of elements that other installed elements depend on. It stores transitive dependencies, so even indirect dependents block uninstallation. - Upgrade ordering: During upgrade, the old
InstalledManifestis kept alive until the new one is confirmed installed with a linked EM Element. This prevents a gap where the element is temporarily absent from EM. - DummyMigrationRepoDriver: Exists for backward compatibility — older installations that predate repo proxy need their existing elements attached to a repository. This stub driver allows that without requiring a real external repository.
See also#
- Core Developer Guide — architecture overview, element engine, reconciliation
- Exports reference — how elements share resources
- Manifest reference — manifest YAML specification