Skip to content

Commit

Permalink
Add a new notebook_runtime script where includes the logic to create/…
Browse files Browse the repository at this point in the history
…watch/update a new ConfigMap the `pipeline-runtime-images` for runtime images and mount it as volume on the notebook when is getting created
  • Loading branch information
atheo89 committed Feb 20, 2025
1 parent 672e102 commit 9dc2dd8
Show file tree
Hide file tree
Showing 5 changed files with 268 additions and 4 deletions.
6 changes: 3 additions & 3 deletions components/odh-notebook-controller/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
include makefile-vars.mk

# Image URL to use all building/pushing image targets
IMG ?= quay.io/opendatahub/odh-notebook-controller
TAG ?= $(shell git describe --tags --always)
IMG ?= quay.io/rh_ee_atheodor/odh-notebook-controller
TAG ?= runtime-configmapv1

KF_IMG ?= quay.io/opendatahub/kubeflow-notebook-controller
KF_TAG ?= $(KF_TAG)
Expand Down Expand Up @@ -114,7 +114,7 @@ run: manifests generate fmt vet certificates ktunnel ## Run a controller from yo
go run ./main.go

.PHONY: docker-build
docker-build: test ## Build docker image with the manager.
docker-build: ## Build docker image with the manager.
cd ../ && ${CONTAINER_ENGINE} build . -t ${IMG}:${TAG} -f odh-notebook-controller/Dockerfile

.PHONY: docker-push
Expand Down
2 changes: 1 addition & 1 deletion components/odh-notebook-controller/config/base/params.env
Original file line number Diff line number Diff line change
@@ -1 +1 @@
odh-notebook-controller-image=quay.io/opendatahub/odh-notebook-controller:main
odh-notebook-controller-image=quay.io/rh_ee_atheodor/odh-notebook-controller:runtime-configmapv1
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/rest"
"k8s.io/client-go/util/retry"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
Expand All @@ -60,6 +61,7 @@ type OpenshiftNotebookReconciler struct {
Namespace string
Scheme *runtime.Scheme
Log logr.Logger
Config *rest.Config
}

// ClusterRole permissions
Expand Down Expand Up @@ -190,6 +192,11 @@ func (r *OpenshiftNotebookReconciler) Reconcile(ctx context.Context, req ctrl.Re
return ctrl.Result{}, err
}

err = r.EnsureNotebookConfigMap(notebook, ctx)
if err != nil {
return ctrl.Result{}, err
}

// Call the Rolebinding reconciler
if strings.ToLower(strings.TrimSpace(os.Getenv("SET_PIPELINE_RBAC"))) == "true" {
err = r.ReconcileRoleBindings(notebook, ctx)
Expand Down
251 changes: 251 additions & 0 deletions components/odh-notebook-controller/controllers/notebook_runtime.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
package controllers

import (
"context"
"encoding/json"
"strings"

"github.com/go-logr/logr"
nbv1 "github.com/kubeflow/kubeflow/components/notebook-controller/api/v1"
corev1 "k8s.io/api/core/v1"
apierrs "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
)

// ConfigMap name for runtime images
const configMapName = "pipeline-runtime-images"

// checkConfigMapExists verifies if a ConfigMap exists in the namespace.
func (r *OpenshiftNotebookReconciler) checkConfigMapExists(ctx context.Context, configMapName, namespace string) (bool, error) {
configMap := &corev1.ConfigMap{}
err := r.Get(ctx, types.NamespacedName{Name: configMapName, Namespace: namespace}, configMap)
if err != nil {
if apierrs.IsNotFound(err) {
return false, nil // ConfigMap not found
}
return false, err // Some other error occurred
}
//r.Log.Info("ConfigMap found", "ConfigMap.Name", configMapName, "Namespace", namespace)
return true, nil // ConfigMap exists
}

func (r *OpenshiftNotebookReconciler) syncRuntimeImagesConfigMap(ctx context.Context, notebookNamespace string) error {
log := r.Log.WithValues("namespace", notebookNamespace)

// Create a dynamic client
config, err := rest.InClusterConfig()
if err != nil {
log.Error(err, "Error creating cluster config")
return err
}
dynamicClient, err := dynamic.NewForConfig(config)
if err != nil {
log.Error(err, "Error creating dynamic client")
return err
}

// Define GroupVersionResource for ImageStreams
ims := schema.GroupVersionResource{
Group: "image.openshift.io",
Version: "v1",
Resource: "imagestreams",
}

// Fetch ImageStreams from "redhat-ods-applications" namespace
imageStreamNamespace := "redhat-ods-applications"
imagestreams, err := dynamicClient.Resource(ims).Namespace(imageStreamNamespace).List(ctx, metav1.ListOptions{})
if err != nil {
log.Error(err, "Failed to list ImageStreams", "Namespace", imageStreamNamespace)
return err
}

// Prepare data for ConfigMap
data := make(map[string]string)
for _, item := range imagestreams.Items {
labels := item.GetLabels()
if labels["opendatahub.io/runtime-image"] == "true" {
tags, found, err := unstructured.NestedSlice(item.Object, "spec", "tags")
if err != nil || !found {
log.Error(err, "Failed to extract tags from ImageStream", "ImageStream", item.GetName())
continue
}

for _, tag := range tags {
tagMap, ok := tag.(map[string]interface{})
if !ok {
continue
}

// Extract metadata annotation
annotations, found, err := unstructured.NestedMap(tagMap, "annotations")
if err != nil || !found {
annotations = map[string]interface{}{}
}

metadataRaw, ok := annotations["opendatahub.io/runtime-image-metadata"].(string)
if !ok || metadataRaw == "" {
metadataRaw = "[]"
}

// Parse metadata
metadataParsed := parseRuntimeImageMetadata(metadataRaw)
displayName := extractDisplayName(metadataParsed)

// Construct the key name
if displayName != "" {
formattedName := formatKeyName(displayName)
data[formattedName] = metadataParsed
}
}
}
}

// Create or update the ConfigMap in the Notebook's namespace
configMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: configMapName,
Namespace: notebookNamespace,
Labels: map[string]string{"opendatahub.io/managed-by": "workbenches"},
},
Data: data,
}

configMapExists, err := r.checkConfigMapExists(ctx, configMapName, notebookNamespace)
if err != nil {
log.Error(err, "Error checking if ConfigMap exists", "ConfigMap.Name", configMapName)
return err
}

if configMapExists {
existingConfigMap := &corev1.ConfigMap{}
if err := r.Get(ctx, types.NamespacedName{Name: configMapName, Namespace: notebookNamespace}, existingConfigMap); err != nil {
log.Error(err, "Failed to get existing ConfigMap", "ConfigMap.Name", configMapName)
return err
}

if !jsonEqual(existingConfigMap.Data, data) {
existingConfigMap.Data = data
if err := r.Update(ctx, existingConfigMap); err != nil {
log.Error(err, "Failed to update ConfigMap", "ConfigMap.Name", configMapName)
return err
}
log.Info("Updated existing ConfigMap with new runtime images", "ConfigMap.Name", configMapName)
}
} else {
if err := r.Create(ctx, configMap); err != nil {
log.Error(err, "Failed to create ConfigMap", "ConfigMap.Name", configMapName)
return err
}
log.Info("Created new ConfigMap for runtime images", "ConfigMap.Name", configMapName)
}

return nil
}

// jsonEqual compares two JSON-like maps for equality.
func jsonEqual(a, b map[string]string) bool {
aBytes, _ := json.Marshal(a)
bBytes, _ := json.Marshal(b)
return string(aBytes) == string(bBytes)
}

func extractDisplayName(metadata string) string {
var metadataMap map[string]interface{}
err := json.Unmarshal([]byte(metadata), &metadataMap)
if err != nil {
return ""
}
displayName, ok := metadataMap["display_name"].(string)
if !ok {
return ""
}
return displayName
}

func formatKeyName(displayName string) string {
replacer := strings.NewReplacer(" ", "-", "(", "", ")", "")
return strings.ToLower(replacer.Replace(displayName)) + ".json"
}

// parseRuntimeImageMetadata extracts the first object from the JSON array
func parseRuntimeImageMetadata(rawJSON string) string {
var metadataArray []map[string]interface{}

err := json.Unmarshal([]byte(rawJSON), &metadataArray)
if err != nil || len(metadataArray) == 0 {
return "{}" // Return empty JSON object if parsing fails
}

// Convert first object back to JSON
metadataJSON, err := json.Marshal(metadataArray[0])
if err != nil {
return "{}"
}

return string(metadataJSON)
}

func (r *OpenshiftNotebookReconciler) EnsureNotebookConfigMap(notebook *nbv1.Notebook, ctx context.Context) error {
return r.syncRuntimeImagesConfigMap(ctx, notebook.Namespace)
}

func MountPipelineRuntimeImages(notebook *nbv1.Notebook, log logr.Logger) error {

configMapName := "pipeline-runtime-images"
mountPath := "/opt/app-root/pipeline-runtimes/"
volumeName := "runtime-images"

// Define the volume
configVolume := corev1.Volume{
Name: volumeName,
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: configMapName,
},
},
},
}

// Define the volume mount
volumeMount := corev1.VolumeMount{
Name: volumeName,
MountPath: mountPath,
}

// Append the volume if it does not already exist
volumes := &notebook.Spec.Template.Spec.Volumes
volumeExists := false
for _, v := range *volumes {
if v.Name == volumeName {
volumeExists = true
break
}
}
if !volumeExists {
*volumes = append(*volumes, configVolume)
}

log.Info("Injecting runtime-images volume into notebook", "notebook", notebook.Name, "namespace", notebook.Namespace)

// Append the volume mount to all containers
for i, container := range notebook.Spec.Template.Spec.Containers {
mountExists := false
for _, vm := range container.VolumeMounts {
if vm.Name == volumeName {
mountExists = true
break
}
}
if !mountExists {
notebook.Spec.Template.Spec.Containers[i].VolumeMounts = append(container.VolumeMounts, volumeMount)
}
}

return nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,12 @@ func (w *NotebookWebhook) Handle(ctx context.Context, req admission.Request) adm
return admission.Errored(http.StatusInternalServerError, err)
}

// Mount ConfigMap pipeline-runtime-images
err = MountPipelineRuntimeImages(notebook, log)
if err != nil {
return admission.Errored(http.StatusInternalServerError, err)
}

}

// Check Imagestream Info both on create and update operations
Expand Down

0 comments on commit 9dc2dd8

Please sign in to comment.