-
Notifications
You must be signed in to change notification settings - Fork 39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WIP: Populate Elyra runtime images configMap from runtime images imagestream manifests and mounts it on the Notebook CR #513
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. just a note that code in this file is not run in envtest, according to codecov There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you Jiri for your review! I will try to fix that, maybe i will reach you out to at some point to consult me with this new test framework. |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,248 @@ | ||||||||||||||||||||||||||||||||||||
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" | ||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
const configMapName = "pipeline-runtime-images" | ||||||||||||||||||||||||||||||||||||
const mountPath = "/opt/app-root/pipeline-runtimes/" | ||||||||||||||||||||||||||||||||||||
const volumeName = "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 | ||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||
Comment on lines
+41
to
+51
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
// 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 { | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
// 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 := ¬ebook.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 | ||||||||||||||||||||||||||||||||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
set this in
main.go
?and also in
suite_test.go
for tests?