July 30, 20265 minutes
Platforms like OpenControlPlane run service providers that install and manage services (Flux, Crossplane, Kro, …) on behalf of their users. Each service provider needs to know which versions of a service are available and where to fetch the corresponding artifacts from. Today, that means hardcoding OCI references (image URLs, chart registries, pull secrets) into provider configs, per version, per service:
apiVersion: flux.services.open-control-plane.io/v1alpha1
kind: ProviderConfig
metadata:
name: flux
spec:
versions:
- version: "2.8.3"
chartVersion: "2.18.2"
chartUrl: "oci://ghcr.io/fluxcd-community/charts/flux2"
chartPullSecret: "chart-registry-credentials"
values:
helmController:
image: my-registry.example.com/fluxcd/helm-controller
tag: v1.5.3
sourceController:
image: my-registry.example.com/fluxcd/source-controller
tag: v1.8.1
- version: "2.18.2"
chartVersion: "2.18.2"
chartUrl: "oci://ghcr.io/fluxcd-community/charts/flux2"
chartPullSecret: "chart-registry-credentials"
values:
helmController:
image: my-registry.example.com/fluxcd/helm-controller
tag: v1.5.3
sourceController:
image: my-registry.example.com/fluxcd/source-controller
tag: v1.8.1
# ... repeated per versionAdd a version? Edit YAML. Switch registries for an air-gapped environment? Edit more YAML. Ask “what’s available?” Grep and hope.
OCM (Open Component Model) already models this: components reference other components, components carry resources (images, charts), the whole graph is transportable. But there was no way to query it from inside a cluster. Controllers couldn’t ask “what versions of Flux exist?” or “give me all images for Kro v0.9.2.”
Instead of scattering artifact locations across controller configs, model your entire service catalog as an OCM component tree. Conceptually, an umbrella component references all available services and versions (abbreviated for illustration, not a full component descriptor):
- name: ocm.software/service-catalog
version: "1.0.0"
componentReferences:
- name: flux
componentName: ocm.software/service-catalog/flux
version: "2.8.3"
- name: flux
componentName: ocm.software/service-catalog/flux
version: "2.18.2"
- name: ocm
componentName: ocm.software/kubernetes/controller
version: "0.9.0"
- name: ocm
componentName: ocm.software/kubernetes/controller
version: "0.10.0"
- name: ocm
componentName: ocm.software/kubernetes/controller
version: "0.11.0"
- name: kro
componentName: ocm.software/service-catalog/kro
version: v0.9.0
- name: kro
componentName: ocm.software/service-catalog/kro
version: v0.9.1
- name: kro
componentName: ocm.software/service-catalog/kro
version: v0.9.2Each referenced component in turn carries its actual artifacts (again shown in abbreviated form):
- name: ocm.software/service-catalog/kro
version: v0.9.2
resources:
- name: kro
type: helmChart
access:
type: ociArtifact
imageReference: registry.k8s.io/kro/charts/kro:0.9.2
- name: image-kro
type: ociImage
access:
type: ociArtifact
imageReference: registry.k8s.io/kro/kro:v0.9.2Full service-catalog example: gist
The structure is there. What was missing: a way to query it declaratively from within the cluster. So we built a controller that makes this graph queryable via a single Kubernetes resource.
We introduced a new CRD: Discovery. Point it at a Component object, declare selectors, and the controller resolves the graph for you.
“Give me all Kro versions >= v0.9.1”, returning the complete OCM descriptors:
apiVersion: delivery.ocm.software/v1alpha1
kind: Discovery
metadata:
name: kro-versions
spec:
componentRef:
name: service-catalog
referenceSelector:
matchIdentity:
name: "ocm.software/service-catalog/kro"
matchExpressions:
- key: version
operator: SemverRange
values: [">= 0.9.1"]Result in status.discovery:
[
{
"component": {
"name": "ocm.software/service-catalog/kro",
"provider": "ocm.software",
"version": "v0.9.1",
"resources": [
{
"name": "kro",
"type": "helmChart",
"access": {
"type": "ociArtifact",
"imageReference": "registry.k8s.io/kro/charts/kro:0.9.1@sha256:37b00031..."
},
"digest": { "hashAlgorithm": "SHA-256", "value": "37b00031..." }
},
{
"name": "image-kro",
"type": "ociImage",
"access": {
"type": "ociArtifact",
"imageReference": "registry.k8s.io/kro/kro:v0.9.1@sha256:f460b2d7..."
},
"digest": { "hashAlgorithm": "SHA-256", "value": "f460b2d7..." }
}
]
},
"meta": { "schemaVersion": "v2" }
},
{
"component": {
"name": "ocm.software/service-catalog/kro",
"provider": "ocm.software",
"version": "v0.9.2",
"resources": ["..."]
},
"meta": { "schemaVersion": "v2" }
}
]Full descriptors with digests and access info. Everything your controller needs to verify and pull.
Same query, but extracting only what you need into a flat, consumable format:
apiVersion: delivery.ocm.software/v1alpha1
kind: Discovery
metadata:
name: kro-charts
spec:
componentRef:
name: service-catalog
referenceSelector:
matchIdentity:
name: "ocm.software/service-catalog/kro"
matchExpressions:
- key: version
operator: SemverRange
values: [">= 0.9.1"]
resourceSelector:
matchIdentity:
name: kro
discoveryFields:
version: "component.version"
chart: "resource.access.imageReference"Result:
[
{
"chart": "registry.k8s.io/kro/charts/kro:0.9.1@sha256:37b00031...",
"version": "v0.9.1"
},
{
"chart": "registry.k8s.io/kro/charts/kro:0.9.2@sha256:bafeda52...",
"version": "v0.9.2"
}
]Two fields, no noise. Ready to be consumed by any downstream controller without OCM-specific logic.
The Discovery controller:
componentReferences recursively using OCM’s DAG library, resolving nested trees in parallel.referenceSelector to filter which referenced components match.resourceSelector to filter resources within each matched component.discoveryFields using dot-notation paths (resource.access.imageReference, component.version) to extract exactly what consumers need into a flat array.Both selectors follow the same pattern as Kubernetes label selectors (matchLabels, matchExpressions with In, NotIn, Exists, DoesNotExist), extended with matchIdentity and a SemverRange operator.
The Discovery controller watches the referenced Component CR. When the component version changes (e.g. because the Component CR is configured with an interval or a semver constraint that picks up a new release), the Discovery re-reconciles automatically.
This is a proof of concept. Open questions and next steps:
Component CR, but referenced components are not yet verified individually. The trade-off: verifying each reference means downloading the descriptor and computing a checksum, which may be too expensive for large graphs.Upstream issue: open-component-model/ocm-project#1153