Skip to content

Commit 29c1146

Browse files
authored
Merge pull request #593 from harshad16/allow-nb-namespace-imageselection
RHOAIENG-23765: Adjust the image selection to consider notebook namespace
2 parents 8041e65 + 2f05e1a commit 29c1146

File tree

7 files changed

+201
-131
lines changed

7 files changed

+201
-131
lines changed

components/odh-notebook-controller/config/rbac/role.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ rules:
3333
verbs:
3434
- get
3535
- list
36+
- watch
3637
- apiGroups:
3738
- kubeflow.org
3839
resources:

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ type OpenshiftNotebookReconciler struct {
7676
// +kubebuilder:rbac:groups=oauth.openshift.io,resources=oauthclients,verbs=get;list;watch;create;update;patch
7777
// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles,verbs=get;list;watch;create;update;patch
7878
// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=rolebindings,verbs=get;list;watch;create;update;patch;delete
79-
// +kubebuilder:rbac:groups="image.openshift.io",resources=imagestreams,verbs=list;get
79+
// +kubebuilder:rbac:groups="image.openshift.io",resources=imagestreams,verbs=list;get;watch
8080

8181
// CompareNotebooks checks if two notebooks are equal, if not return false.
8282
func CompareNotebooks(nb1 nbv1.Notebook, nb2 nbv1.Notebook) bool {
@@ -137,7 +137,7 @@ func (r *OpenshiftNotebookReconciler) RemoveReconciliationLock(notebook *nbv1.No
137137
return err
138138
}
139139
if len(serviceAccount.ImagePullSecrets) == 0 {
140-
return errors.New("Pull secret not mounted")
140+
return errors.New("pull secret not mounted")
141141
}
142142
return nil
143143
},
@@ -358,7 +358,7 @@ func (r *OpenshiftNotebookReconciler) CreateNotebookCertConfigMap(notebook *nbv1
358358
r.Log.Info("Created workbench-trusted-ca-bundle ConfigMap", "namespace", notebook.Namespace, "notebook", notebook.Name)
359359
}
360360
}
361-
} else if err == nil && !reflect.DeepEqual(foundTrustedCAConfigMap.Data, desiredTrustedCAConfigMap.Data) {
361+
} else if !reflect.DeepEqual(foundTrustedCAConfigMap.Data, desiredTrustedCAConfigMap.Data) {
362362
// some data has changed, update the ConfigMap
363363
r.Log.Info("Updating workbench-trusted-ca-bundle ConfigMap", "namespace", notebook.Namespace, "notebook", notebook.Name)
364364
foundTrustedCAConfigMap.Data = desiredTrustedCAConfigMap.Data

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -939,7 +939,12 @@ var _ = Describe("The Openshift Notebook controller", func() {
939939
})
940940

941941
It("Should not create OAuth secret", func() {
942-
secrets := &corev1.SecretList{}
942+
secrets := &corev1.SecretList{
943+
TypeMeta: metav1.TypeMeta{
944+
Kind: "SecretList",
945+
APIVersion: "v1",
946+
},
947+
}
943948
Eventually(func() error {
944949
return cli.List(context.Background(), secrets, client.InNamespace(namespace))
945950
}, duration, interval).Should(Succeed())

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

Lines changed: 64 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,19 @@ import (
2929
"go.opentelemetry.io/otel/trace"
3030

3131
configv1 "github.com/openshift/api/config/v1"
32+
imagev1 "github.com/openshift/api/image/v1"
3233

3334
"github.com/go-logr/logr"
3435
nbv1 "github.com/kubeflow/kubeflow/components/notebook-controller/api/v1"
3536
"github.com/kubeflow/kubeflow/components/notebook-controller/pkg/culler"
3637
admissionv1 "k8s.io/api/admission/v1"
3738
corev1 "k8s.io/api/core/v1"
3839
"k8s.io/apimachinery/pkg/api/equality"
39-
k8serr "k8s.io/apimachinery/pkg/api/errors"
40+
apierrs "k8s.io/apimachinery/pkg/api/errors"
4041
"k8s.io/apimachinery/pkg/api/resource"
4142
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
42-
"k8s.io/apimachinery/pkg/runtime/schema"
43+
"k8s.io/apimachinery/pkg/types"
4344
"k8s.io/apimachinery/pkg/util/intstr"
44-
"k8s.io/client-go/dynamic"
4545
"k8s.io/client-go/rest"
4646
"k8s.io/utils/ptr"
4747
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -70,7 +70,8 @@ var getWebhookTracer func() trace.Tracer = sync.OnceValue(func() trace.Tracer {
7070
})
7171

7272
const (
73-
IMAGE_STREAM_NOT_FOUND_EVENT = "imagestream-not-found"
73+
IMAGE_STREAM_NOT_FOUND_EVENT = "imagestream-not-found"
74+
IMAGE_STREAM_TAG_NOT_FOUND_EVENT = "imagestream-tag-not-found"
7475
)
7576

7677
// InjectReconciliationLock injects the kubeflow notebook controller culling
@@ -338,7 +339,7 @@ func (w *NotebookWebhook) Handle(ctx context.Context, req admission.Request) adm
338339
// Check Imagestream Info both on create and update operations
339340
if req.Operation == admissionv1.Create || req.Operation == admissionv1.Update {
340341
// Check Imagestream Info
341-
err = SetContainerImageFromRegistry(ctx, w.Config, notebook, log, w.Namespace)
342+
err = SetContainerImageFromRegistry(ctx, w.Client, notebook, log, w.Namespace)
342343
if err != nil {
343344
return admission.Errored(http.StatusInternalServerError, err)
344345
}
@@ -688,23 +689,16 @@ func InjectCertConfig(notebook *nbv1.Notebook, configMapName string) error {
688689
// If an internal registry is detected, it uses the default values specified in the Notebook Custom Resource (CR).
689690
// Otherwise, it checks the last-image-selection annotation to find the image stream and fetches the image from status.dockerImageReference,
690691
// assigning it to the container.image value.
691-
func SetContainerImageFromRegistry(ctx context.Context, config *rest.Config, notebook *nbv1.Notebook, log logr.Logger, namespace string) error {
692+
func SetContainerImageFromRegistry(ctx context.Context, cli client.Client, notebook *nbv1.Notebook, log logr.Logger, controllerNamespace string) error {
692693
span := trace.SpanFromContext(ctx)
693694

694-
// Create a dynamic client
695-
dynamicClient, err := dynamic.NewForConfig(config)
696-
if err != nil {
697-
log.Error(err, "Error creating dynamic client")
698-
return err
699-
}
700-
701695
annotations := notebook.GetAnnotations()
702696
if annotations != nil {
703697
if imageSelection, exists := annotations["notebooks.opendatahub.io/last-image-selection"]; exists {
704698

705699
containerFound := false
706700
// Iterate over containers to find the one matching the notebook name
707-
for i, container := range notebook.Spec.Template.Spec.Containers {
701+
for _, container := range notebook.Spec.Template.Spec.Containers {
708702
if container.Name == notebook.Name {
709703
containerFound = true
710704

@@ -722,70 +716,72 @@ func SetContainerImageFromRegistry(ctx context.Context, config *rest.Config, not
722716
return fmt.Errorf("invalid image selection format")
723717
}
724718

725-
// Specify the GroupVersionResource for imagestreams
726-
ims := schema.GroupVersionResource{
727-
Group: "image.openshift.io",
728-
Version: "v1",
729-
Resource: "imagestreams",
730-
}
731-
732719
imagestreamFound := false
733-
// List imagestreams in the specified namespace
734-
imagestreams, err := dynamicClient.Resource(ims).Namespace(namespace).List(ctx, metav1.ListOptions{})
735-
if err != nil {
736-
if k8serr.IsForbidden(err) {
737-
log.Info("Permission denied to list imagestreams", "namespace", namespace, "error", err)
738-
// fast exit on permission denied
739-
return err
720+
imageTagFound := false
721+
imagestreamName := imageSelected[0]
722+
imgSelection := &imagev1.ImageStream{}
723+
724+
// Search for the ImageStream in the controller namespace first
725+
// As default, the ImageStream is created in the controller namespace
726+
// if not found, search in the notebook namespace
727+
// Note: This is in this order, so users should not overwrite the ImageStream
728+
err := cli.Get(ctx, types.NamespacedName{Name: imagestreamName, Namespace: controllerNamespace}, imgSelection)
729+
if err != nil && apierrs.IsNotFound(err) {
730+
log.Info("Unable to find the ImageStream in controller namespace, try finding in notebook namespace", "imagestream", imagestreamName, "controllerNamespace", controllerNamespace)
731+
// Check if the ImageStream is present in the notebook namespace
732+
err = cli.Get(ctx, types.NamespacedName{Name: imagestreamName, Namespace: notebook.Namespace}, imgSelection)
733+
if err != nil {
734+
log.Error(err, "Error getting ImageStream", "imagestream", imagestreamName, "controllerNamespace", controllerNamespace)
735+
} else {
736+
// ImageStream found in the notebook namespace
737+
imagestreamFound = true
738+
log.Info("ImageStream found in notebook namespace", "imagestream", imagestreamName, "namespace", notebook.Namespace)
740739
}
741-
log.Info("Cannot list imagestreams", "namespace", namespace, "error", err)
742-
continue
740+
} else {
741+
// ImageStream found in the controller namespace
742+
imagestreamFound = true
743+
log.Info("ImageStream found in controller namespace", "imagestream", imagestreamName, "controllerNamespace", controllerNamespace)
743744
}
744745

745-
// Iterate through the imagestreams to find matches
746-
for _, item := range imagestreams.Items {
747-
metadata := item.Object["metadata"].(map[string]interface{})
748-
name := metadata["name"].(string)
749-
750-
// Match with the ImageStream name
751-
if name == imageSelected[0] {
752-
status := item.Object["status"].(map[string]interface{})
753-
754-
// Match to the corresponding tag of the image
755-
if tags, ok := status["tags"].([]interface{}); ok && tags != nil {
756-
for _, t := range tags {
757-
tagMap := t.(map[string]interface{})
758-
tagName := tagMap["tag"].(string)
759-
if tagName == imageSelected[1] {
760-
if items, ok := tagMap["items"].([]interface{}); ok && items != nil && len(items) > 0 {
761-
// Sort items by creationTimestamp to get the most recent one
762-
sort.Slice(items, func(i, j int) bool {
763-
iTime := items[i].(map[string]interface{})["created"].(string)
764-
jTime := items[j].(map[string]interface{})["created"].(string)
765-
return iTime > jTime // Lexicographical comparison of RFC3339 timestamps
766-
})
767-
imageHash := items[0].(map[string]interface{})["dockerImageReference"].(string)
768-
// Update the Containers[i].Image value
769-
notebook.Spec.Template.Spec.Containers[i].Image = imageHash
770-
// Update the JUPYTER_IMAGE environment variable with the image selection for example "jupyter-datascience-notebook:2023.2"
771-
for i, envVar := range container.Env {
772-
if envVar.Name == "JUPYTER_IMAGE" {
773-
container.Env[i].Value = imageSelection
774-
break
775-
}
776-
}
777-
imagestreamFound = true
746+
if imagestreamFound {
747+
// Check if the ImageStream has a status and tags
748+
if imgSelection.Status.Tags == nil {
749+
log.Error(nil, "ImageStream has no status or tags", "name", imagestreamName, "namespace", controllerNamespace)
750+
span.AddEvent(IMAGE_STREAM_TAG_NOT_FOUND_EVENT)
751+
return fmt.Errorf("ImageStream has no status or tags")
752+
}
753+
// Iterate through the tags to find the one matching the imageSelected
754+
for _, tag := range imgSelection.Status.Tags {
755+
// Check if the tag name matches the imageSelected
756+
if tag.Tag == imageSelected[1] {
757+
// Check if the items are present
758+
if len(tag.Items) > 0 {
759+
// Sort items by creationTimestamp to get the most recent one
760+
sort.Slice(tag.Items, func(i, j int) bool {
761+
iTime := tag.Items[i].Created
762+
jTime := tag.Items[j].Created
763+
return iTime.Time.After(jTime.Time) //nolint:QF1008 // Reason: We are comparing metav1.Time // Lexicographical comparison of RFC3339 timestamps
764+
})
765+
// Get the most recent item
766+
imageHash := tag.Items[0].DockerImageReference
767+
// Update the container image
768+
notebook.Spec.Template.Spec.Containers[0].Image = imageHash
769+
// Update the JUPYTER_IMAGE environment variable with the image selection for example "jupyter-datascience-notebook:2023.2"
770+
for i, envVar := range container.Env {
771+
if envVar.Name == "JUPYTER_IMAGE" {
772+
container.Env[i].Value = imageSelection
778773
break
779774
}
780775
}
776+
imageTagFound = true
777+
break
781778
}
782779
}
783780
}
784-
}
785-
if !imagestreamFound {
786-
span.AddEvent(IMAGE_STREAM_NOT_FOUND_EVENT)
787-
log.Error(nil, "ImageStream not found in main controller namespace, or the ImageStream is present but does not contain a dockerImageReference for the specified tag",
788-
"imageSelected", imageSelected[0], "tag", imageSelected[1], "namespace", namespace)
781+
if !imageTagFound {
782+
log.Error(nil, "ImageStream is present but does not contain a dockerImageReference for the specified tag")
783+
span.AddEvent(IMAGE_STREAM_TAG_NOT_FOUND_EVENT)
784+
}
789785
}
790786
}
791787
}

0 commit comments

Comments
 (0)