@@ -29,19 +29,19 @@ import (
29
29
"go.opentelemetry.io/otel/trace"
30
30
31
31
configv1 "github.com/openshift/api/config/v1"
32
+ imagev1 "github.com/openshift/api/image/v1"
32
33
33
34
"github.com/go-logr/logr"
34
35
nbv1 "github.com/kubeflow/kubeflow/components/notebook-controller/api/v1"
35
36
"github.com/kubeflow/kubeflow/components/notebook-controller/pkg/culler"
36
37
admissionv1 "k8s.io/api/admission/v1"
37
38
corev1 "k8s.io/api/core/v1"
38
39
"k8s.io/apimachinery/pkg/api/equality"
39
- k8serr "k8s.io/apimachinery/pkg/api/errors"
40
+ apierrs "k8s.io/apimachinery/pkg/api/errors"
40
41
"k8s.io/apimachinery/pkg/api/resource"
41
42
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
42
- "k8s.io/apimachinery/pkg/runtime/schema "
43
+ "k8s.io/apimachinery/pkg/types "
43
44
"k8s.io/apimachinery/pkg/util/intstr"
44
- "k8s.io/client-go/dynamic"
45
45
"k8s.io/client-go/rest"
46
46
"k8s.io/utils/ptr"
47
47
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -70,7 +70,8 @@ var getWebhookTracer func() trace.Tracer = sync.OnceValue(func() trace.Tracer {
70
70
})
71
71
72
72
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"
74
75
)
75
76
76
77
// InjectReconciliationLock injects the kubeflow notebook controller culling
@@ -338,7 +339,7 @@ func (w *NotebookWebhook) Handle(ctx context.Context, req admission.Request) adm
338
339
// Check Imagestream Info both on create and update operations
339
340
if req .Operation == admissionv1 .Create || req .Operation == admissionv1 .Update {
340
341
// Check Imagestream Info
341
- err = SetContainerImageFromRegistry (ctx , w .Config , notebook , log , w .Namespace )
342
+ err = SetContainerImageFromRegistry (ctx , w .Client , notebook , log , w .Namespace )
342
343
if err != nil {
343
344
return admission .Errored (http .StatusInternalServerError , err )
344
345
}
@@ -688,23 +689,16 @@ func InjectCertConfig(notebook *nbv1.Notebook, configMapName string) error {
688
689
// If an internal registry is detected, it uses the default values specified in the Notebook Custom Resource (CR).
689
690
// Otherwise, it checks the last-image-selection annotation to find the image stream and fetches the image from status.dockerImageReference,
690
691
// 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 {
692
693
span := trace .SpanFromContext (ctx )
693
694
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
-
701
695
annotations := notebook .GetAnnotations ()
702
696
if annotations != nil {
703
697
if imageSelection , exists := annotations ["notebooks.opendatahub.io/last-image-selection" ]; exists {
704
698
705
699
containerFound := false
706
700
// 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 {
708
702
if container .Name == notebook .Name {
709
703
containerFound = true
710
704
@@ -722,70 +716,72 @@ func SetContainerImageFromRegistry(ctx context.Context, config *rest.Config, not
722
716
return fmt .Errorf ("invalid image selection format" )
723
717
}
724
718
725
- // Specify the GroupVersionResource for imagestreams
726
- ims := schema.GroupVersionResource {
727
- Group : "image.openshift.io" ,
728
- Version : "v1" ,
729
- Resource : "imagestreams" ,
730
- }
731
-
732
719
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 )
740
739
}
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 )
743
744
}
744
745
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
778
773
break
779
774
}
780
775
}
776
+ imageTagFound = true
777
+ break
781
778
}
782
779
}
783
780
}
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
+ }
789
785
}
790
786
}
791
787
}
0 commit comments