Skip to content

Commit 8cba60e

Browse files
authored
Populate Elyra runtime images configMap and mounts it on the Notebook (#576)
* Populate Elyra runtime images configMap from runtime images imagestreams and mounts it on the Notebook CR * create a new testing file * Add seperated test cases e2e for runtime configmap * remove runtime imagestream while the initialization * Fix review comment * fix typo error
1 parent 03fbca3 commit 8cba60e

File tree

7 files changed

+469
-1
lines changed

7 files changed

+469
-1
lines changed

.github/workflows/odh_notebook_controller_integration_test.yaml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,29 @@ jobs:
125125
x-kubernetes-preserve-unknown-fields: true
126126
served: true
127127
storage: true
128+
---
129+
apiVersion: apiextensions.k8s.io/v1
130+
kind: CustomResourceDefinition
131+
metadata:
132+
annotations:
133+
crd/fake: "true"
134+
name: imagestreams.image.openshift.io
135+
spec:
136+
group: image.openshift.io
137+
names:
138+
kind: ImageStream
139+
listKind: ImageStreamList
140+
singular: imagestream
141+
plural: imagestreams
142+
scope: Namespaced
143+
versions:
144+
- name: v1
145+
schema:
146+
openAPIV3Schema:
147+
type: object
148+
x-kubernetes-preserve-unknown-fields: true
149+
served: true
150+
storage: true
128151
EOF
129152
130153
- name: Build & Apply manifests
@@ -290,3 +313,14 @@ jobs:
290313
kubectl describe pods
291314
kubectl logs minimal-notebook-0
292315
kubectl describe routes
316+
317+
- name: Print logs (again, at the end)
318+
if: "!cancelled()"
319+
run: |
320+
kubectl describe pods -n kubeflow -l app=notebook-controller
321+
kubectl logs -n kubeflow -l app=notebook-controller
322+
- name: Print ODH logs (again, at the end)
323+
if: "!cancelled()"
324+
run: |
325+
kubectl describe pods -n opendatahub -l app=odh-notebook-controller
326+
kubectl logs -n opendatahub -l app=odh-notebook-controller

components/odh-notebook-controller/controllers/notebook_controller.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import (
4040
"k8s.io/apimachinery/pkg/runtime"
4141
"k8s.io/apimachinery/pkg/types"
4242
"k8s.io/apimachinery/pkg/util/wait"
43+
"k8s.io/client-go/rest"
4344
"k8s.io/client-go/util/retry"
4445
ctrl "sigs.k8s.io/controller-runtime"
4546
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -60,6 +61,7 @@ type OpenshiftNotebookReconciler struct {
6061
Namespace string
6162
Scheme *runtime.Scheme
6263
Log logr.Logger
64+
Config *rest.Config
6365
}
6466

6567
// ClusterRole permissions
@@ -191,6 +193,12 @@ func (r *OpenshiftNotebookReconciler) Reconcile(ctx context.Context, req ctrl.Re
191193
return ctrl.Result{}, err
192194
}
193195

196+
// Create/Watch and Update the pipeline-runtime-image ConfigMap on Notebook's Namespace
197+
err = r.EnsureNotebookConfigMap(notebook, ctx)
198+
if err != nil {
199+
return ctrl.Result{}, err
200+
}
201+
194202
// Call the Rolebinding reconciler
195203
if strings.ToLower(strings.TrimSpace(os.Getenv("SET_PIPELINE_RBAC"))) == "true" {
196204
err = r.ReconcileRoleBindings(notebook, ctx)
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
package controllers
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"reflect"
7+
"strings"
8+
9+
"github.com/go-logr/logr"
10+
nbv1 "github.com/kubeflow/kubeflow/components/notebook-controller/api/v1"
11+
corev1 "k8s.io/api/core/v1"
12+
apierrs "k8s.io/apimachinery/pkg/api/errors"
13+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
15+
"k8s.io/apimachinery/pkg/runtime/schema"
16+
"k8s.io/apimachinery/pkg/types"
17+
"k8s.io/client-go/dynamic"
18+
"k8s.io/client-go/rest"
19+
"k8s.io/utils/ptr"
20+
"sigs.k8s.io/controller-runtime/pkg/client"
21+
)
22+
23+
const (
24+
configMapName = "pipeline-runtime-images"
25+
mountPath = "/opt/app-root/pipeline-runtimes/"
26+
volumeName = "runtime-images"
27+
)
28+
29+
// getRuntimeConfigMap verifies if a ConfigMap exists in the namespace.
30+
func (r *OpenshiftNotebookReconciler) getRuntimeConfigMap(ctx context.Context, configMapName, namespace string) (*corev1.ConfigMap, bool, error) {
31+
configMap := &corev1.ConfigMap{}
32+
err := r.Get(ctx, types.NamespacedName{Name: configMapName, Namespace: namespace}, configMap)
33+
if err != nil {
34+
if apierrs.IsNotFound(err) {
35+
return nil, false, nil
36+
}
37+
return nil, false, err
38+
}
39+
return configMap, true, nil
40+
}
41+
42+
func (r *OpenshiftNotebookReconciler) syncRuntimeImagesConfigMap(ctx context.Context, notebookNamespace string, controllerNamespace string, config *rest.Config) error {
43+
44+
log := r.Log.WithValues("namespace", notebookNamespace)
45+
46+
// Create a dynamic client
47+
dynamicClient, err := dynamic.NewForConfig(config)
48+
if err != nil {
49+
log.Error(err, "Error creating dynamic client")
50+
return err
51+
}
52+
53+
// Define GroupVersionResource for ImageStreams
54+
ims := schema.GroupVersionResource{
55+
Group: "image.openshift.io",
56+
Version: "v1",
57+
Resource: "imagestreams",
58+
}
59+
60+
// Fetch ImageStreams from controllerNamespace namespace
61+
imagestreams, err := dynamicClient.Resource(ims).Namespace(controllerNamespace).List(ctx, metav1.ListOptions{})
62+
if err != nil {
63+
log.Error(err, "Failed to list ImageStreams", "Namespace", controllerNamespace)
64+
return err
65+
}
66+
67+
// Prepare data for ConfigMap
68+
data := make(map[string]string)
69+
for _, item := range imagestreams.Items {
70+
labels := item.GetLabels()
71+
if labels["opendatahub.io/runtime-image"] == "true" {
72+
tags, found, err := unstructured.NestedSlice(item.Object, "spec", "tags")
73+
if err != nil || !found {
74+
log.Error(err, "Failed to extract tags from ImageStream", "ImageStream", item.GetName())
75+
continue
76+
}
77+
78+
for _, tag := range tags {
79+
tagMap, ok := tag.(map[string]interface{})
80+
if !ok {
81+
continue
82+
}
83+
84+
// Extract metadata annotation
85+
annotations, found, err := unstructured.NestedMap(tagMap, "annotations")
86+
if err != nil || !found {
87+
annotations = map[string]interface{}{}
88+
}
89+
metadataRaw, ok := annotations["opendatahub.io/runtime-image-metadata"].(string)
90+
if !ok || metadataRaw == "" {
91+
metadataRaw = "[]"
92+
}
93+
94+
// Parse metadata
95+
metadataParsed := parseRuntimeImageMetadata(metadataRaw)
96+
displayName := extractDisplayName(metadataParsed)
97+
98+
// Construct the key name
99+
if displayName != "" {
100+
formattedName := formatKeyName(displayName)
101+
data[formattedName] = metadataParsed
102+
}
103+
}
104+
}
105+
}
106+
107+
// Check if the ConfigMap already exists
108+
existingConfigMap, configMapExists, err := r.getRuntimeConfigMap(ctx, configMapName, notebookNamespace)
109+
if err != nil {
110+
log.Error(err, "Error getting ConfigMap", "ConfigMap.Name", configMapName)
111+
return err
112+
}
113+
114+
// If data is empty and ConfigMap does not exist, skip creating anything
115+
if len(data) == 0 && !configMapExists {
116+
log.Info("No runtime images found. Skipping creation of empty ConfigMap.")
117+
return nil
118+
}
119+
120+
// If data is empty and ConfigMap does exist, decide what behavior we want:
121+
if len(data) == 0 && configMapExists {
122+
log.Info("Data is empty but the ConfigMap already exists. Leaving it as is.")
123+
// OR optionally delete it:
124+
// if err := r.Delete(ctx, existingConfigMap); err != nil {
125+
// log.Error(err, "Failed to delete existing empty ConfigMap")
126+
// return err
127+
//}
128+
return nil
129+
}
130+
131+
// Create a new ConfigMap struct with the data
132+
configMap := &corev1.ConfigMap{
133+
ObjectMeta: metav1.ObjectMeta{
134+
Name: configMapName,
135+
Namespace: notebookNamespace,
136+
Labels: map[string]string{"opendatahub.io/managed-by": "workbenches"},
137+
},
138+
Data: data,
139+
}
140+
141+
// If the ConfigMap exists and data has changed, update it
142+
if configMapExists {
143+
if !reflect.DeepEqual(existingConfigMap.Data, data) {
144+
existingConfigMap.Data = data
145+
if err := r.Update(ctx, existingConfigMap); err != nil {
146+
log.Error(err, "Failed to update ConfigMap", "ConfigMap.Name", configMapName)
147+
return err
148+
}
149+
log.Info("Updated existing ConfigMap with new runtime images", "ConfigMap.Name", configMapName)
150+
} else {
151+
log.Info("ConfigMap already up-to-date", "ConfigMap.Name", configMapName)
152+
}
153+
return nil
154+
}
155+
156+
// Otherwise, create the ConfigMap
157+
if err := r.Create(ctx, configMap); err != nil {
158+
log.Error(err, "Failed to create ConfigMap", "ConfigMap.Name", configMapName)
159+
return err
160+
}
161+
log.Info("Created new ConfigMap for runtime images", "ConfigMap.Name", configMapName)
162+
163+
return nil
164+
}
165+
166+
func extractDisplayName(metadata string) string {
167+
var metadataMap map[string]interface{}
168+
err := json.Unmarshal([]byte(metadata), &metadataMap)
169+
if err != nil {
170+
return ""
171+
}
172+
displayName, ok := metadataMap["display_name"].(string)
173+
if !ok {
174+
return ""
175+
}
176+
return displayName
177+
}
178+
179+
func formatKeyName(displayName string) string {
180+
replacer := strings.NewReplacer(" ", "-", "(", "", ")", "")
181+
return strings.ToLower(replacer.Replace(displayName)) + ".json"
182+
}
183+
184+
// parseRuntimeImageMetadata extracts the first object from the JSON array
185+
func parseRuntimeImageMetadata(rawJSON string) string {
186+
var metadataArray []map[string]interface{}
187+
188+
err := json.Unmarshal([]byte(rawJSON), &metadataArray)
189+
if err != nil || len(metadataArray) == 0 {
190+
return "{}" // Return empty JSON object if parsing fails
191+
}
192+
193+
// Convert first object back to JSON
194+
metadataJSON, err := json.Marshal(metadataArray[0])
195+
if err != nil {
196+
return "{}"
197+
}
198+
199+
return string(metadataJSON)
200+
}
201+
202+
func (r *OpenshiftNotebookReconciler) EnsureNotebookConfigMap(notebook *nbv1.Notebook, ctx context.Context) error {
203+
return r.syncRuntimeImagesConfigMap(ctx, notebook.Namespace, r.Namespace, r.Config)
204+
}
205+
206+
func MountPipelineRuntimeImages(ctx context.Context, client client.Client, notebook *nbv1.Notebook, log logr.Logger) error {
207+
208+
// Retrieve the ConfigMap
209+
configMap := &corev1.ConfigMap{}
210+
err := client.Get(ctx, types.NamespacedName{Name: configMapName, Namespace: notebook.Namespace}, configMap)
211+
if err != nil {
212+
if apierrs.IsNotFound(err) {
213+
log.Info("ConfigMap does not exist", "ConfigMap", configMapName)
214+
return nil
215+
}
216+
log.Error(err, "Error retrieving ConfigMap", "ConfigMap", configMapName)
217+
return err
218+
}
219+
220+
// Check if the ConfigMap is empty
221+
if len(configMap.Data) == 0 {
222+
log.Info("ConfigMap is empty, skipping volume mount", "ConfigMap", configMapName)
223+
return nil
224+
}
225+
226+
// Define the volume
227+
configVolume := corev1.Volume{
228+
Name: volumeName,
229+
VolumeSource: corev1.VolumeSource{
230+
ConfigMap: &corev1.ConfigMapVolumeSource{
231+
LocalObjectReference: corev1.LocalObjectReference{
232+
Name: configMapName,
233+
},
234+
Optional: ptr.To(true),
235+
},
236+
},
237+
}
238+
239+
// Define the volume mount
240+
volumeMount := corev1.VolumeMount{
241+
Name: volumeName,
242+
MountPath: mountPath,
243+
}
244+
245+
// Append the volume if it does not already exist
246+
volumes := &notebook.Spec.Template.Spec.Volumes
247+
volumeExists := false
248+
for _, v := range *volumes {
249+
if v.Name == volumeName {
250+
volumeExists = true
251+
break
252+
}
253+
}
254+
if !volumeExists {
255+
*volumes = append(*volumes, configVolume)
256+
}
257+
258+
log.Info("Injecting runtime-images volume into notebook", "notebook", notebook.Name, "namespace", notebook.Namespace)
259+
260+
// Append the volume mount to all containers
261+
for i, container := range notebook.Spec.Template.Spec.Containers {
262+
mountExists := false
263+
for _, vm := range container.VolumeMounts {
264+
if vm.Name == volumeName {
265+
mountExists = true
266+
break
267+
}
268+
}
269+
if !mountExists {
270+
notebook.Spec.Template.Spec.Containers[i].VolumeMounts = append(container.VolumeMounts, volumeMount)
271+
}
272+
}
273+
274+
return nil
275+
}

0 commit comments

Comments
 (0)