Skip to content

Commit 3408b8c

Browse files
harshad16jstourac
andauthored
RHOAI-4148: Re-adjust the statefulset, route, service based on generateName (#539)
* Adjust the route url to maintain the sub-domain length less than 63 char Signed-off-by: Harshad Reddy Nalla <[email protected]> * Adjust the statefulset naming based on the length Signed-off-by: Harshad Reddy Nalla <[email protected]> * Use constant vars for routelengthmax and namespacemax Signed-off-by: Harshad Reddy Nalla <[email protected]> * Include the env test case for the route generateName Signed-off-by: Harshad Reddy Nalla <[email protected]> * Check limit for entire sub-domain instead of name and namespace Signed-off-by: Harshad Reddy Nalla <[email protected]> * adjust the finding of route for oauthclient Signed-off-by: Harshad Reddy Nalla <[email protected]> * Update components/notebook-controller/controllers/notebook_controller.go Co-authored-by: Jan Stourac <[email protected]> * Fix the lint issue - ST1023: should omit type bool from declaration; it will be inferred from the right-hand side (staticcheck) - QF1008: could remove embedded field "ObjectMeta" from selector (staticcheck) Signed-off-by: Harshad Reddy Nalla <[email protected]> --------- Signed-off-by: Harshad Reddy Nalla <[email protected]> Co-authored-by: Jan Stourac <[email protected]>
1 parent b5e7966 commit 3408b8c

File tree

4 files changed

+340
-51
lines changed

4 files changed

+340
-51
lines changed

components/notebook-controller/controllers/notebook_controller.go

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ const WorkbenchLabel = "opendatahub.io/workbenches"
5555

5656
const PrefixEnvVar = "NB_PREFIX"
5757

58+
// Statefulset name should be less than 52https://github.com/kubernetes/kubernetes/issues/64023
59+
const MaxStatefulsetNameLength = 52
60+
5861
// The default fsGroup of PodSecurityContext.
5962
// https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.11/#podsecuritycontext-v1-core
6063
const DefaultFSGroup = int64(100)
@@ -136,16 +139,38 @@ func (r *NotebookReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
136139
return ctrl.Result{}, nil
137140
}
138141

142+
// check if Notebook Name length is greater than 52
143+
// as controller-service include as hash (len 11) as label
144+
// controller-service-hash that shouldn't exceed 63 chars.
145+
isGenerateName := false
146+
if len(instance.Name) > MaxStatefulsetNameLength {
147+
log.Info("Notebook name is too long, it can be {MaxStatefulsetNameLength} chars long at most")
148+
isGenerateName = true
149+
}
150+
139151
// Reconcile StatefulSet
140-
ss := generateStatefulSet(instance)
152+
ss := generateStatefulSet(instance, isGenerateName)
141153
if err := ctrl.SetControllerReference(instance, ss, r.Scheme); err != nil {
142154
return ctrl.Result{}, err
143155
}
144156
// Check if the StatefulSet already exists
145157
foundStateful := &appsv1.StatefulSet{}
158+
namespacedStatefulSets := &appsv1.StatefulSetList{}
159+
err := r.List(ctx, namespacedStatefulSets, client.InNamespace(ss.Namespace))
160+
if err != nil {
161+
log.Error(err, "error listing StatefulSets")
162+
return ctrl.Result{}, err
163+
}
164+
165+
for _, sts := range namespacedStatefulSets.Items {
166+
if metav1.IsControlledBy(&sts, instance) {
167+
foundStateful = &sts
168+
break
169+
}
170+
}
171+
146172
justCreated := false
147-
err := r.Get(ctx, types.NamespacedName{Name: ss.Name, Namespace: ss.Namespace}, foundStateful)
148-
if err != nil && apierrs.IsNotFound(err) {
173+
if foundStateful.Name == "" && foundStateful.Namespace == "" {
149174
log.Info("Creating StatefulSet", "namespace", ss.Namespace, "name", ss.Name)
150175
r.Metrics.NotebookCreation.WithLabelValues(ss.Namespace).Inc()
151176
err = r.Create(ctx, ss)
@@ -405,17 +430,25 @@ func setPrefixEnvVar(instance *v1beta1.Notebook, container *corev1.Container) {
405430
})
406431
}
407432

408-
func generateStatefulSet(instance *v1beta1.Notebook) *appsv1.StatefulSet {
433+
func generateStatefulSet(instance *v1beta1.Notebook, isGenerateName bool) *appsv1.StatefulSet {
409434
replicas := int32(1)
410435
if metav1.HasAnnotation(instance.ObjectMeta, "kubeflow-resource-stopped") {
411436
replicas = 0
412437
}
413438

439+
ssObjectMeta := metav1.ObjectMeta{
440+
Name: instance.Name,
441+
Namespace: instance.Namespace,
442+
}
443+
if isGenerateName {
444+
ssObjectMeta = metav1.ObjectMeta{
445+
GenerateName: "nb-",
446+
Namespace: instance.Namespace,
447+
}
448+
}
449+
414450
ss := &appsv1.StatefulSet{
415-
ObjectMeta: metav1.ObjectMeta{
416-
Name: instance.Name,
417-
Namespace: instance.Namespace,
418-
},
451+
ObjectMeta: ssObjectMeta,
419452
Spec: appsv1.StatefulSetSpec{
420453
Replicas: &replicas,
421454
Selector: &metav1.LabelSelector{
@@ -501,7 +534,7 @@ func generateService(instance *v1beta1.Notebook) *corev1.Service {
501534
Ports: []corev1.ServicePort{
502535
{
503536
// Make port name follow Istio pattern so it can be managed by istio rbac
504-
Name: "http-" + instance.Name,
537+
Name: "http-notebook",
505538
Port: DefaultServingPort,
506539
TargetPort: intstr.FromInt(port),
507540
Protocol: "TCP",

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

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"context"
2020
"crypto/x509"
2121
"encoding/pem"
22+
"errors"
2223
"fmt"
2324
"os"
2425
"time"
@@ -313,6 +314,131 @@ var _ = Describe("The Openshift Notebook controller", func() {
313314

314315
})
315316

317+
// New test case for notebook creation with long name
318+
When("Creating a long named Notebook", func() {
319+
320+
// With the work done https://issues.redhat.com/browse/RHOAIENG-4148,
321+
// 48 characters is the maximum length for a notebook name.
322+
// This would the extent of the test.
323+
// TODO: Update the test to use the maximum length when the work is done.
324+
const (
325+
Name = "test-notebook-with-a-very-long-name-thats-48char"
326+
Namespace = "default"
327+
)
328+
329+
notebook := createNotebook(Name, Namespace)
330+
331+
expectedRoute := routev1.Route{
332+
ObjectMeta: metav1.ObjectMeta{
333+
GenerateName: "nb-",
334+
Namespace: Namespace,
335+
Labels: map[string]string{
336+
"notebook-name": Name,
337+
},
338+
},
339+
Spec: routev1.RouteSpec{
340+
To: routev1.RouteTargetReference{
341+
Kind: "Service",
342+
Name: Name,
343+
Weight: ptr.To[int32](100),
344+
},
345+
Port: &routev1.RoutePort{
346+
TargetPort: intstr.FromString("http-" + Name),
347+
},
348+
TLS: &routev1.TLSConfig{
349+
Termination: routev1.TLSTerminationEdge,
350+
InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyRedirect,
351+
},
352+
WildcardPolicy: routev1.WildcardPolicyNone,
353+
},
354+
Status: routev1.RouteStatus{
355+
Ingress: []routev1.RouteIngress{},
356+
},
357+
}
358+
359+
route := &routev1.Route{}
360+
361+
It("Should create a Route to expose the traffic externally", func() {
362+
ctx := context.Background()
363+
364+
By("By creating a new Notebook")
365+
Expect(cli.Create(ctx, notebook)).Should(Succeed())
366+
time.Sleep(interval)
367+
368+
By("By checking that the controller has created the Route")
369+
Eventually(func() error {
370+
route, err := getRouteFromList(route, notebook, Name, Namespace)
371+
if route == nil {
372+
return err
373+
}
374+
return nil
375+
}, duration, interval).Should(Succeed())
376+
Expect(*route).To(BeMatchingK8sResource(expectedRoute, CompareNotebookRoutes))
377+
})
378+
379+
It("Should reconcile the Route when modified", func() {
380+
By("By simulating a manual Route modification")
381+
patch := client.RawPatch(types.MergePatchType, []byte(`{"spec":{"to":{"name":"foo"}}}`))
382+
Expect(cli.Patch(ctx, route, patch)).Should(Succeed())
383+
time.Sleep(interval)
384+
385+
By("By checking that the controller has restored the Route spec")
386+
Eventually(func() (string, error) {
387+
route, err := getRouteFromList(route, notebook, Name, Namespace)
388+
if route == nil {
389+
return "", err
390+
}
391+
return route.Spec.To.Name, nil
392+
}, duration, interval).Should(Equal(Name))
393+
Expect(*route).To(BeMatchingK8sResource(expectedRoute, CompareNotebookRoutes))
394+
})
395+
396+
It("Should recreate the Route when deleted", func() {
397+
By("By deleting the notebook route")
398+
Expect(cli.Delete(ctx, route)).Should(Succeed())
399+
time.Sleep(interval)
400+
401+
By("By checking that the controller has recreated the Route")
402+
Eventually(func() error {
403+
route, err := getRouteFromList(route, notebook, Name, Namespace)
404+
if route == nil {
405+
return err
406+
}
407+
return nil
408+
}, duration, interval).Should(Succeed())
409+
Expect(*route).To(BeMatchingK8sResource(expectedRoute, CompareNotebookRoutes))
410+
})
411+
412+
It("Should delete the Openshift Route", func() {
413+
// Testenv cluster does not implement Kubernetes GC:
414+
// https://book.kubebuilder.io/reference/envtest.html#testing-considerations
415+
// To test that the deletion lifecycle works, test the ownership
416+
// instead of asserting on existence.
417+
expectedOwnerReference := metav1.OwnerReference{
418+
APIVersion: "kubeflow.org/v1",
419+
Kind: "Notebook",
420+
Name: Name,
421+
UID: notebook.GetObjectMeta().GetUID(),
422+
Controller: ptr.To(true),
423+
BlockOwnerDeletion: ptr.To(true),
424+
}
425+
426+
By("By checking that the Notebook owns the Route object")
427+
Expect(route.GetObjectMeta().GetOwnerReferences()).To(ContainElement(expectedOwnerReference))
428+
429+
By("By deleting the recently created Notebook")
430+
Expect(cli.Delete(ctx, notebook)).Should(Succeed())
431+
time.Sleep(interval)
432+
433+
By("By checking that the Notebook is deleted")
434+
Eventually(func() error {
435+
key := types.NamespacedName{Name: Name, Namespace: Namespace}
436+
return cli.Get(ctx, key, notebook)
437+
}, duration, interval).Should(HaveOccurred())
438+
})
439+
440+
})
441+
316442
// New test case for notebook update
317443
When("Updating a Notebook", func() {
318444
const (
@@ -972,6 +1098,28 @@ func createNotebook(name, namespace string) *nbv1.Notebook {
9721098
}
9731099
}
9741100

1101+
func getRouteFromList(route *routev1.Route, notebook *nbv1.Notebook, name, namespace string) (*routev1.Route, error) {
1102+
routeList := &routev1.RouteList{}
1103+
opts := []client.ListOption{
1104+
client.InNamespace(namespace),
1105+
client.MatchingLabels{"notebook-name": name},
1106+
}
1107+
1108+
err := cli.List(ctx, routeList, opts...)
1109+
if err != nil {
1110+
return nil, err
1111+
}
1112+
1113+
// Get the route from the list
1114+
for _, nRoute := range routeList.Items {
1115+
if metav1.IsControlledBy(&nRoute, notebook) {
1116+
*route = nRoute
1117+
return route, nil
1118+
}
1119+
}
1120+
return nil, errors.New("Route not found")
1121+
}
1122+
9751123
func createOAuthServiceAccount(name, namespace string) corev1.ServiceAccount {
9761124
return corev1.ServiceAccount{
9771125
ObjectMeta: metav1.ObjectMeta{

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

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -275,18 +275,32 @@ func (r *OpenshiftNotebookReconciler) ReconcileOAuthSecret(notebook *nbv1.Notebo
275275
func (r *OpenshiftNotebookReconciler) ReconcileOAuthClient(notebook *nbv1.Notebook, ctx context.Context) error {
276276
log := logf.FromContext(ctx)
277277

278+
// Create the route if it does not already exist
278279
route := &routev1.Route{}
279-
err := r.Get(ctx, types.NamespacedName{
280-
Name: notebook.Name,
281-
Namespace: notebook.Namespace,
282-
}, route)
280+
routeList := &routev1.RouteList{}
281+
282+
// List the routes in the notebook namespace with the notebook name label
283+
opts := []client.ListOption{
284+
client.InNamespace(notebook.Namespace),
285+
client.MatchingLabels{"notebook-name": notebook.Name},
286+
}
287+
288+
err := r.List(ctx, routeList, opts...)
283289
if err != nil {
284-
if apierrs.IsNotFound(err) {
285-
log.Info("Route not found, cannot create OAuthClient yet", "route", notebook.Name)
286-
return nil
290+
log.Error(err, "Unable to list the Route")
291+
}
292+
293+
// Get the route from the list
294+
for _, nRoute := range routeList.Items {
295+
if metav1.IsControlledBy(&nRoute, notebook) {
296+
route = &nRoute
297+
break
287298
}
288-
log.Error(err, "Failed to get Route for OAuthClient")
289-
return err
299+
}
300+
301+
if route.Name == "" && route.Namespace != notebook.Namespace {
302+
log.Info("Route not found, cannot create OAuthClient yet", "route", notebook.Name)
303+
return nil
290304
}
291305

292306
err = r.createOAuthClient(notebook, ctx)
@@ -335,8 +349,8 @@ func (r *OpenshiftNotebookReconciler) createSecret(notebook *nbv1.Notebook, ctx
335349
}
336350

337351
// NewNotebookOAuthRoute defines the desired OAuth route object
338-
func NewNotebookOAuthRoute(notebook *nbv1.Notebook) *routev1.Route {
339-
route := NewNotebookRoute(notebook)
352+
func NewNotebookOAuthRoute(notebook *nbv1.Notebook, isGenerateName bool) *routev1.Route {
353+
route := NewNotebookRoute(notebook, isGenerateName)
340354
route.Spec.To.Name = notebook.Name + "-tls"
341355
route.Spec.Port.TargetPort = intstr.FromString(OAuthServicePortName)
342356
route.Spec.TLS.Termination = routev1.TLSTerminationReencrypt

0 commit comments

Comments
 (0)