diff --git a/docs/COMPONENT_INTEGRATION.md b/docs/COMPONENT_INTEGRATION.md new file mode 100644 index 00000000000..953b7a21a9e --- /dev/null +++ b/docs/COMPONENT_INTEGRATION.md @@ -0,0 +1,324 @@ +# Component Integration + +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](https://github.com/opendatahub-io/opendatahub-operator/blob/main/docs/DESIGN.md). + +The list of the currently integrated ODH components is provided [at the end of this document](#integrated-components). + +## Integrating a new component + +To ensure a new component is integrated seamlessly in the operator, please follow the steps listed below. + +### 1. Update API specs + +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: + +#### Define internal API spec for the new component + +1. Create a dedicated `_types.go` file within `apis/components/v1alpha1` directory. + +2. Define the internal API spec for the new component according to the expected definitions. +You can use the following pseudo-implementation for reference: + +```go +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. + +#### Add Component to DataScienceCluster API spec + +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. + +```diff +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. + +```diff +// 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"` +} +``` + +#### Update kubebuilder_rbac.go + +Add kubebuilder RBAC permissions intended for the new component into `controllers/datasciencecluster/kubebuilder_rbac.go`. + +#### Update the dependent files + +To fully reflect the API changes brought by the addition of the new component, run the following command: +```make +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. + +### 2. Create a module for the new component reconciliation logic + +To add new component-specific reconciler logic, create a dedicated `` module, located in the `controllers/components` directory. +For reference, the `controllers/components` directory contains reconciler implementations for the currently integrated components. + +#### Implement the component handler interface + +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 `.go` file within the newly created component module and provide the interface implementation: + +```go +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. + +#### Implement new component reconciler + +Create a dedicated `_controller.go` file and implement the expected `NewComponentReconciler` function there. +This function will be responsible for creating the reconciler for the previously introduced `` 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. manifest rendering), as well as custom, component-specific actions + - more details on actions are provided [below](#actions) + +The example pseudo-implementation should look like as follows: +```go +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 + +Actions are functions that define pieces of component reconciliation logic. Any action is expected to conform to the following signature: + +```go +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 `_controller_actions.go`. + +"Generic"/commonly-implemented actions for each of the currently integrated components include: +- `initialize()` - to register paths to the component manifests +- `devFlags()` - 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: +- (if necessary) creating pod security role binding +- 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** + +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. + +#### Update upgrade.go + +Update the `CreateDefaultDSC()` function in `pkg/upgrade/upgrade.go` to include the newly added component. + +#### Update main.go + +Add an import for the the newly added component: + +```diff +package main + +import ( + // ... existing imports ... + + // ... component imports for the integrated components ... ++ _ "github.com/opendatahub-io/opendatahub-operator/v2/controllers/components/" +) +``` + +### 3. Add unit and e2e tests + +Please add `unit` tests for any component-specific functions added to the codebase. + +Please also add [e2e tests](https://github.com/opendatahub-io/opendatahub-operator/tree/main/tests/e2e) 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 in `tests/e2e/helper_test.go` to include the newly added component +- update `newDSC()` function in `controllers/webhook/webhook_suite_test.go` to include the newly added component +- update `componentsTestSuites` map in `tests/e2e/controller_test.go` to include the reference for the new component e2e test suite + +## Integrated components + +Currently integrated components are: +- [Dashboard](https://github.com/opendatahub-io/opendatahub-operator/tree/main/components/dashboard) +- [Codeflare](https://github.com/opendatahub-io/opendatahub-operator/tree/main/components/codeflare) +- [Ray](https://github.com/opendatahub-io/opendatahub-operator/tree/main/components/ray) +- [Data Science Pipelines](https://github.com/opendatahub-io/opendatahub-operator/tree/main/components/datasciencepipelines) +- [KServe](https://github.com/opendatahub-io/opendatahub-operator/tree/main/components/kserve) +- [ModelMesh Serving](https://github.com/opendatahub-io/opendatahub-operator/tree/main/components/modelmeshserving) +- [Workbenches](https://github.com/opendatahub-io/opendatahub-operator/tree/main/components/workbenches) +- [TrustyAI](https://github.com/opendatahub-io/opendatahub-operator/tree/main/components/trustyai) +- [ModelRegistry](https://github.com/opendatahub-io/opendatahub-operator/tree/main/components/modelregistry) +- [Kueue](https://github.com/opendatahub-io/kueue) +- [Model Controller](https://github.com/opendatahub-io/odh-model-controller) + +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`.