Skip to content
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

RHOAIENG-8390: feat(odh-nbc): search for imagestreams only in the namespace the controller runs in #375

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 All @@ -462,9 +455,9 @@ var _ = Describe("The Openshift Notebook controller", func() {
},
From: []netv1.NetworkPolicyPeer{
{
// Since for unit tests we do not have context,
// namespace will fallback to test pod namespace
// if run in CI or `redhat-ods-applications` if run locally
// Since for unit tests the controller does not run in a cluster pod,
// it cannot detect its own pod's namespace. Therefore, we define it
// to be `redhat-ods-applications` (in suite_test.go)
NamespaceSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"kubernetes.io/metadata.name": testPodNamespace,
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,
shalberd marked this conversation as resolved.
Show resolved Hide resolved
},
}
// 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
shalberd marked this conversation as resolved.
Show resolved Hide resolved
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")
shalberd marked this conversation as resolved.
Show resolved Hide resolved
}

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