Skip to content

Commit

Permalink
odh notebook controller imagestreams logic: do not hard-code namespac…
Browse files Browse the repository at this point in the history
…e to look in. Take namespace controller runs in instead. Log that dynamcically determined namespace once at startup. Use hard-coded namespace for unit tests.
  • Loading branch information
shalberd committed Nov 26, 2024
1 parent f1be2a4 commit e67e164
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 90 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,9 @@ const (
// OpenshiftNotebookReconciler holds the controller configuration.
type OpenshiftNotebookReconciler struct {
client.Client
Scheme *runtime.Scheme
Log logr.Logger
Namespace string
Scheme *runtime.Scheme
Log logr.Logger
}

// ClusterRole permissions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@ import (
"crypto/x509"
"encoding/pem"
"fmt"
"io/ioutil"
"os"
"strings"
"time"

"github.com/go-logr/logr"
Expand Down Expand Up @@ -432,12 +430,7 @@ var _ = Describe("The Openshift Notebook controller", func() {
notebook := createNotebook(Name, Namespace)

npProtocol := corev1.ProtocolTCP
testPodNamespace := "redhat-ods-applications"
if data, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace"); err == nil {
if ns := strings.TrimSpace(string(data)); len(ns) > 0 {
testPodNamespace = ns
}
}
testPodNamespace := odhNotebookControllerTestNamespace

expectedNotebookNetworkPolicy := netv1.NetworkPolicy{
ObjectMeta: metav1.ObjectMeta{
Expand Down
21 changes: 4 additions & 17 deletions components/odh-notebook-controller/controllers/notebook_network.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,9 @@ import (
"context"
corev1 "k8s.io/api/core/v1"
netv1 "k8s.io/api/networking/v1"
"os"
"reflect"
"strings"

"github.com/go-logr/logr"
nbv1 "github.com/kubeflow/kubeflow/components/notebook-controller/api/v1"
apierrs "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand All @@ -44,7 +43,7 @@ func (r *OpenshiftNotebookReconciler) ReconcileAllNetworkPolicies(notebook *nbv1
log := r.Log.WithValues("notebook", notebook.Name, "namespace", notebook.Namespace)

// Generate the desired Network Policies
desiredNotebookNetworkPolicy := NewNotebookNetworkPolicy(notebook)
desiredNotebookNetworkPolicy := NewNotebookNetworkPolicy(notebook, log, r.Namespace)

// Create Network Policies if they do not already exist
err := r.reconcileNetworkPolicy(desiredNotebookNetworkPolicy, ctx, notebook)
Expand Down Expand Up @@ -129,11 +128,11 @@ func CompareNotebookNetworkPolicies(np1 netv1.NetworkPolicy, np2 netv1.NetworkPo
}

// NewNotebookNetworkPolicy defines the desired network policy for Notebook port
func NewNotebookNetworkPolicy(notebook *nbv1.Notebook) *netv1.NetworkPolicy {
func NewNotebookNetworkPolicy(notebook *nbv1.Notebook, log logr.Logger, namespace string) *netv1.NetworkPolicy {
npProtocol := corev1.ProtocolTCP
namespaceSel := metav1.LabelSelector{
MatchLabels: map[string]string{
"kubernetes.io/metadata.name": getControllerNamespace(),
"kubernetes.io/metadata.name": namespace,
},
}
// Create a Kubernetes NetworkPolicy resource that allows all traffic to the oauth port of a notebook
Expand Down Expand Up @@ -209,15 +208,3 @@ func NewOAuthNetworkPolicy(notebook *nbv1.Notebook) *netv1.NetworkPolicy {
},
}
}

func getControllerNamespace() string {
// TODO:Add env variable that stores namespace for both controllers.
if data, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace"); err == nil {
if ns := strings.TrimSpace(string(data)); len(ns) > 0 {
return ns
}
}

// Fallback to default namespace, keep default as redhat-ods-applications
return "redhat-ods-applications"
}
93 changes: 44 additions & 49 deletions components/odh-notebook-controller/controllers/notebook_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ type NotebookWebhook struct {
Config *rest.Config
Decoder *admission.Decoder
OAuthConfig OAuthConfig
// controller namespace
Namespace string
}

// InjectReconciliationLock injects the kubeflow notebook controller culling
Expand Down Expand Up @@ -254,7 +256,7 @@ func (w *NotebookWebhook) Handle(ctx context.Context, req admission.Request) adm
// Check Imagestream Info both on create and update operations
if req.Operation == admissionv1.Create || req.Operation == admissionv1.Update {
// Check Imagestream Info
err = SetContainerImageFromRegistry(ctx, w.Config, notebook, log)
err = SetContainerImageFromRegistry(ctx, w.Config, notebook, log, w.Namespace)
if err != nil {
return admission.Errored(http.StatusInternalServerError, err)
}
Expand Down Expand Up @@ -536,7 +538,7 @@ func InjectCertConfig(notebook *nbv1.Notebook, configMapName string) error {
// If an internal registry is detected, it uses the default values specified in the Notebook Custom Resource (CR).
// Otherwise, it checks the last-image-selection annotation to find the image stream and fetches the image from status.dockerImageReference,
// assigning it to the container.image value.
func SetContainerImageFromRegistry(ctx context.Context, config *rest.Config, notebook *nbv1.Notebook, log logr.Logger) error {
func SetContainerImageFromRegistry(ctx context.Context, config *rest.Config, notebook *nbv1.Notebook, log logr.Logger, namespace string) error {
// Create a dynamic client
dynamicClient, err := dynamic.NewForConfig(config)
if err != nil {
Expand Down Expand Up @@ -575,63 +577,56 @@ func SetContainerImageFromRegistry(ctx context.Context, config *rest.Config, not
return fmt.Errorf("invalid image selection format")
}

// Specify the namespaces to search in
namespaces := []string{"opendatahub", "redhat-ods-applications"}
imagestreamFound := false
for _, namespace := range namespaces {
// List imagestreams in the specified namespace
imagestreams, err := dynamicClient.Resource(ims).Namespace(namespace).List(ctx, metav1.ListOptions{})
if err != nil {
log.Info("Cannot list imagestreams", "error", err)
continue
}
// List imagestreams in the specified namespace
imagestreams, err := dynamicClient.Resource(ims).Namespace(namespace).List(ctx, metav1.ListOptions{})
if err != nil {
log.Info("Cannot list imagestreams", "error", err)
continue
}

// Iterate through the imagestreams to find matches
for _, item := range imagestreams.Items {
metadata := item.Object["metadata"].(map[string]interface{})
name := metadata["name"].(string)

// Match with the ImageStream name
if name == imageSelected[0] {
status := item.Object["status"].(map[string]interface{})

// Match to the corresponding tag of the image
tags := status["tags"].([]interface{})
for _, t := range tags {
tagMap := t.(map[string]interface{})
tagName := tagMap["tag"].(string)
if tagName == imageSelected[1] {
items := tagMap["items"].([]interface{})
if len(items) > 0 {
// Sort items by creationTimestamp to get the most recent one
sort.Slice(items, func(i, j int) bool {
iTime := items[i].(map[string]interface{})["created"].(string)
jTime := items[j].(map[string]interface{})["created"].(string)
return iTime > jTime // Lexicographical comparison of RFC3339 timestamps
})
imageHash := items[0].(map[string]interface{})["dockerImageReference"].(string)
// Update the Containers[i].Image value
notebook.Spec.Template.Spec.Containers[i].Image = imageHash
// Update the JUPYTER_IMAGE environment variable with the image selection for example "jupyter-datascience-notebook:2023.2"
for i, envVar := range container.Env {
if envVar.Name == "JUPYTER_IMAGE" {
container.Env[i].Value = imageSelection
break
}
// Iterate through the imagestreams to find matches
for _, item := range imagestreams.Items {
metadata := item.Object["metadata"].(map[string]interface{})
name := metadata["name"].(string)

// Match with the ImageStream name
if name == imageSelected[0] {
status := item.Object["status"].(map[string]interface{})

// Match to the corresponding tag of the image
tags := status["tags"].([]interface{})
for _, t := range tags {
tagMap := t.(map[string]interface{})
tagName := tagMap["tag"].(string)
if tagName == imageSelected[1] {
items := tagMap["items"].([]interface{})
if len(items) > 0 {
// Sort items by creationTimestamp to get the most recent one
sort.Slice(items, func(i, j int) bool {
iTime := items[i].(map[string]interface{})["created"].(string)
jTime := items[j].(map[string]interface{})["created"].(string)
return iTime > jTime // Lexicographical comparison of RFC3339 timestamps
})
imageHash := items[0].(map[string]interface{})["dockerImageReference"].(string)
// Update the Containers[i].Image value
notebook.Spec.Template.Spec.Containers[i].Image = imageHash
// Update the JUPYTER_IMAGE environment variable with the image selection for example "jupyter-datascience-notebook:2023.2"
for i, envVar := range container.Env {
if envVar.Name == "JUPYTER_IMAGE" {
container.Env[i].Value = imageSelection
break
}
imagestreamFound = true
break
}
imagestreamFound = true
break
}
}
}
}
if imagestreamFound {
break
}
}
if !imagestreamFound {
log.Error(nil, "Imagestream not found in any of the specified namespaces", "imageSelected", imageSelected[0], "tag", imageSelected[1])
log.Error(nil, "Imagestream not found in main controller namespace", "imageSelected", imageSelected[0], "tag", imageSelected[1], "namespace", namespace)
}
}
}
Expand Down
19 changes: 11 additions & 8 deletions components/odh-notebook-controller/controllers/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,9 @@ var (
)

const (
timeout = time.Second * 10
interval = time.Second * 2
timeout = time.Second * 10
interval = time.Second * 2
odhNotebookControllerTestNamespace = "redhat-ods-applications"
)

func TestAPIs(t *testing.T) {
Expand Down Expand Up @@ -173,19 +174,21 @@ var _ = BeforeSuite(func() {

// Setup notebook controller
err = (&OpenshiftNotebookReconciler{
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("notebook-controller"),
Scheme: mgr.GetScheme(),
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("notebook-controller"),
Scheme: mgr.GetScheme(),
Namespace: odhNotebookControllerTestNamespace,
}).SetupWithManager(mgr)
Expect(err).ToNot(HaveOccurred())

// Setup notebook mutating webhook
hookServer := mgr.GetWebhookServer()
notebookWebhook := &webhook.Admission{
Handler: &NotebookWebhook{
Log: ctrl.Log.WithName("controllers").WithName("notebook-controller"),
Client: mgr.GetClient(),
Config: mgr.GetConfig(),
Log: ctrl.Log.WithName("controllers").WithName("notebook-controller"),
Client: mgr.GetClient(),
Config: mgr.GetConfig(),
Namespace: odhNotebookControllerTestNamespace,
OAuthConfig: OAuthConfig{
ProxyImage: OAuthProxyImage,
},
Expand Down
33 changes: 27 additions & 6 deletions components/odh-notebook-controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package main

import (
"flag"
"fmt"
"os"
"time"

Expand Down Expand Up @@ -59,6 +60,17 @@ func init() {
//+kubebuilder:scaffold:scheme
}

func getControllerNamespace() (string, error) {
// Try to get the namespace from the service account secret
if data, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace"); err == nil {
if ns := string(data); len(ns) > 0 {
return ns, nil
}
}

return "", fmt.Errorf("unable to determine the namespace")
}

func main() {
var metricsAddr, probeAddr, oauthProxyImage string
var webhookPort int
Expand Down Expand Up @@ -104,10 +116,18 @@ func main() {
}

// Setup notebook controller
// determine and set the controller namespace
namespace, err := getControllerNamespace()
if err != nil {
setupLog.Error(err, "Error during determining controller / main namespace")
os.Exit(1)
}
setupLog.Info("Controller is running in namespace", "namespace", namespace)
if err = (&controllers.OpenshiftNotebookReconciler{
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("Notebook"),
Scheme: mgr.GetScheme(),
Client: mgr.GetClient(),
Log: ctrl.Log.WithName("controllers").WithName("Notebook"),
Namespace: namespace,
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Notebook")
os.Exit(1)
Expand All @@ -117,9 +137,10 @@ func main() {
hookServer := mgr.GetWebhookServer()
notebookWebhook := &webhook.Admission{
Handler: &controllers.NotebookWebhook{
Log: ctrl.Log.WithName("controllers").WithName("Notebook"),
Client: mgr.GetClient(),
Config: mgr.GetConfig(),
Log: ctrl.Log.WithName("controllers").WithName("Notebook"),
Client: mgr.GetClient(),
Config: mgr.GetConfig(),
Namespace: namespace,
OAuthConfig: controllers.OAuthConfig{
ProxyImage: oauthProxyImage,
},
Expand Down

0 comments on commit e67e164

Please sign in to comment.