Since the ODH operator is the integration point to deploy ODH component manifests, it is essential to have common processes to integrate new components.
Currently, each component is expected to have its own dedicated internal API/CRD and dedicated reconciler. To understand the current operator architecture and its inner workings, please refer to the design document.
The list of the currently integrated ODH components is provided at the end of this document.
To ensure a new component is integrated seamlessly in the operator, please follow the steps listed below.
The first step is to define the internal API spec for the new component and introduce it to the existing DataScienceCluster (DSC) API. Please proceed as follows:
-
Create a dedicated
<example_component_name>_types.go
file withinapis/components/v1alpha1
directory. -
Define the internal API spec for the new component according to the expected definitions. You can use the following pseudo-implementation for reference:
package v1alpha1
import (
"github.com/opendatahub-io/opendatahub-operator/v2/apis/common"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
// example new component name
ExampleComponentName = "examplecomponent"
// ExampleComponentInstanceName is the name of the new component instance singleton
// value should match what is set in the kubebuilder markers for XValidation defined below
ExampleComponentInstanceName = "default-examplecomponent"
// kubernetes kind of the new component
ExampleComponentKind = "ExampleComponent"
)
type ExampleComponentCommonSpec struct {
// new component spec exposed to DSC api
common.DevFlagsSpec `json:",inline"`
// new component spec shared with DSC api
// ( refer/define here if applicable to the new component )
}
// ExampleComponentSpec defines the desired state of ExampleComponent
type ExampleComponentSpec struct {
// new component spec exposed to DSC api
ExampleComponentCommonSpec `json:",inline"`
// new component spec exposed only to internal api
// ( refer/define here if applicable to the new component )
}
// ExampleComponentCommonStatus defines the shared observed state of ExampleComponent
type ExampleComponentCommonStatus struct {
// add fields/attributes if needed
}
// ExampleComponentStatus defines the observed state of ExampleComponent
type ExampleComponentStatus struct {
common.Status `json:",inline"`
ExampleComponentCommonStatus `json:",inline"`
}
// default kubebuilder markers for the new component
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Cluster
// +kubebuilder:validation:XValidation:rule="self.metadata.name == 'default-examplecomponent'",message="ExampleComponent name must be default-examplecomponent"
// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status`,description="Ready"
// +kubebuilder:printcolumn:name="Reason",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].reason`,description="Reason"
// ExampleComponent is the Schema for the new component API
type ExampleComponent struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec ExampleComponentSpec `json:"spec,omitempty"`
Status ExampleComponentStatus `json:"status,omitempty"`
}
// getter for devFlags
func (c *ExampleComponent) GetDevFlags() *common.DevFlags {
return c.Spec.DevFlags
}
// status getter
func (c *ExampleComponent) GetStatus() *common.Status {
return &c.Status.Status
}
// +kubebuilder:object:root=true
// ExampleComponentList contains a list of ExampleComponent
type ExampleComponentList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []ExampleComponent `json:"items"`
}
// register the defined schemas
func init() {
SchemeBuilder.Register(&ExampleComponent{}, &ExampleComponentList{})
}
// DSCExampleComponent contains all the configuration exposed in DSC instance for ExampleComponent component
// ( utilize DSC prefix here for naming consistency with the other integrated components )
type DSCExampleComponent struct {
// configuration fields common across components
common.ManagementSpec `json:",inline"`
// new component-specific fields
ExampleComponentCommonSpec `json:",inline"`
}
// DSCExampleComponentStatus struct holds the status for the ExampleComponent component exposed in the DSC
type DSCExampleComponentStatus struct {
common.ManagementSpec `json:",inline"`
*ExampleComponentCommonStatus `json:",inline"`
}
Alternatively, you can refer to the existing integrated component APIs located within apis/components/v1alpha1
directory.
DataScienceCluster (DSC) CRD is responsible for enabling individual components and exposing them to end users.
To introduce the newly defined component API, extend the Components
struct within the DataScienceCluster API spec (located within apis/datasciencecluster/v1
) to include the new API.
type Components struct {
// Dashboard component configuration.
Dashboard componentApi.DSCDashboard `json:"dashboard,omitempty"`
// Workbenches component configuration.
Workbenches componentApi.DSCWorkbenches `json:"workbenches,omitempty"`
// ... other currently integrated components ...
// add the new component as follows
+ ExampleComponent componentApi.DSCExampleComponent `json:"examplecomponent,omitempty"`
}
Additionally, extend the ComponentsStatus
struct within the same file to include the new component status to be exposed in the DSC.
// ComponentsStatus defines the custom status of DataScienceCluster components.
type ComponentsStatus struct {
// Dashboard component status.
Dashboard componentApi.DSCDashboardStatus `json:"dashboard,omitempty"`
// Workbenches component status.
Workbenches componentApi.DSCWorkbenchesStatus `json:"workbenches,omitempty"`
// ... other currently integrated component statuses ...
// add the new component status as follows
+ ExampleComponent componentApi.DSCExampleComponentStatus `json:"examplecomponent,omitempty"`
}
Add kubebuilder RBAC permissions intended for the new component into controllers/datasciencecluster/kubebuilder_rbac.go
.
To fully reflect the API changes brought by the addition of the new component, run the following command:
make generate manifests api-docs bundle
This command will (re-)generate the necessary kubebuilder functions, and update both the API documentation and the operator bundle manifests.
To add new component-specific reconciler logic, create a dedicated <example_component_name>
module, located in the controllers/components
directory.
For reference, the controllers/components
directory contains reconciler implementations for the currently integrated components.
Each component that is intended to be managed by the operator is expected to be included in the components registry.
The components registry (currently implemented in pkg/componentsregistry
) defines a component handler interface which is required to be implemented for the new component.
To do so, create a dedicated <example_component_name>.go
file within the newly created component module and provide the interface implementation:
type componentHandler struct{}
func init() { //nolint:gochecknoinits
cr.Add(&componentHandler{})
}
func (s *componentHandler) GetName() string
func (s *componentHandler) GetManagementState(dsc *dscv1.DataScienceCluster) operatorv1.ManagementState
func (s *componentHandler) NewCRObject(dsc *dscv1.DataScienceCluster) common.PlatformObject
func (s *componentHandler) Init(platform cluster.Platform) error
func (s *componentHandler) UpdateDSCStatus(dsc *dscv1.DataScienceCluster, obj client.Object) error
Please refer the existing component implementations in the controllers/components
directory for further details.
Create a dedicated <example_component_name>_controller.go
file and implement the expected NewComponentReconciler
function there.
This function will be responsible for creating the reconciler for the previously introduced <ExampleComponent>
API.
NewControllerReconciler
utilizes a generic builder pattern, that supports defining various types of relationships and functionality:
- resource ownership - using
.Owns()
- watching a resource - using
.Watches()
- reconciler actions - using
.WithAction()
- this includes pre-implemented actions used commonly across components (e.g. manifests rendering), as well as customized, component-specific actions
- more details on actions are provided below
The example pseudo-implementation should look like as follows:
func (s *componentHandler) NewComponentReconciler(ctx context.Context, mgr ctrl.Manager) error {
_, err := reconciler.ReconcilerFor(mgr, &componentApi.ExampleComponent{}).
Owns(...).
// ... add other necessary resource ownerships
Watches(...).
// ... add other necessary resource watches
WithAction(...).
// ... add custom actions if needed
// ... add mandatory common actions (e.g. manifest rendering, deployment, garbage collection)
Build(ctx)
if err != nil {
return err
}
return nil
}
Actions are functions that define pieces of component reconciliation logic. Any action is expected to conform to the following signature:
func exampleAction(ctx context.Context, rr *odhtypes.ReconciliationRequest) error
Such actions can be then introduced to the reconciler builder using .WithAction()
calls.
As seen in the existing component reconciler implementations, it would be recommended to include the action implementations in a separate file within the module, such as <example_component_name>_controller_actions.go
.
"Generic"/commonly-implemented actions for each of the currently integrated components include:
initialize()
- to register paths to the component manifestsdevFlags()
- to override the component manifest paths according to the Dev Flags configuration
In addition, proper generic actions, intended to be used across the components, are provided as part of the operator implementation (located in pkg/controller/actions
).
These support:
- manifest rendering
- can additionally utilize caching
- manifest deployment
- can additionally utilize caching
- status updating
- garbage collection
- additional requirement - garbage collection action must always be called as the last action before the final
.Build()
call
- additional requirement - garbage collection action must always be called as the last action before the final
If the new component requires additional custom logic, custom actions can also be added to the builder via the respective .WithAction()
calls.
For practical examples of all the above-mentioned functionality, please refer to the implementations within controllers/components
directory.
Import the newly added component:
package main
import (
// ... existing imports ...
// ... component imports for the integrated components ...
+ _ "github.com/opendatahub-io/opendatahub-operator/v2/controllers/components/<example_component>"
)
Please add unit
tests for any component-specific functions added to the codebase.
Please also add e2e tests to the e2e test suite to capture deployments introduced by the new component. Existing e2e test suites for the integrated components can be also found there.
Lastly, please update the following files to fully integrate new component tests into the overall test suite:
- update
setupDSCInstance()
function intests/e2e/helper_test.go
to set new component in DSC - update
newDSC()
function incontrollers/webhook/webhook_suite_test.go
to update creation of DSC include the new component - update
componentsTestSuites
map intests/e2e/controller_test.go
to include the reference for the new component e2e test suite
If the component is planned to be released for downstream, Prometheus rules and promtest need to be updated for the component.
- Rules are located in
config/monitoring/prometheus/app/prometheus-configs.yaml
file - Tests are grouped in
tests/prometheus_unit_tests
_unit_tests.yam file
Currently integrated components are:
- Codeflare
- Dashboard
- Data Science Pipelines
- KServe
- Kueue
- ModelMesh Serving
- Model Controller
- ModelRegistry
- Ray
- Training Operator
- TrustyAI
- Workbenches
The particular controller implementations for the listed components are located in the controllers/components
directory and the corresponding internal component APIs are located in apis/components/v1alpha1
.