@@ -30,6 +30,7 @@ import (
30
30
. "github.com/onsi/ginkgo/v2"
31
31
. "github.com/onsi/gomega"
32
32
corev1 "k8s.io/api/core/v1"
33
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
33
34
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
34
35
"k8s.io/apimachinery/pkg/types"
35
36
"k8s.io/apimachinery/pkg/util/validation/field"
@@ -510,4 +511,218 @@ metadata:
510
511
))
511
512
})
512
513
})
514
+
515
+ Context ("when creating a WorkspaceKind (POST)" , Serial , func () {
516
+ var newWorkspaceKindName string
517
+ var validYAML []byte
518
+
519
+ BeforeEach (func () {
520
+ newWorkspaceKindName = "wsk-create-test"
521
+
522
+ validYAML = []byte (fmt .Sprintf (`
523
+ apiVersion: kubeflow.org/v1beta1
524
+ kind: WorkspaceKind
525
+ metadata:
526
+ name: %s
527
+ spec:
528
+ spawner:
529
+ displayName: "JupyterLab Notebook"
530
+ description: "A Workspace which runs JupyterLab in a Pod"
531
+ icon:
532
+ url: "https://jupyter.org/assets/favicons/apple-touch-icon.png"
533
+ logo:
534
+ url: "https://jupyter.org/assets/logos/jupyter/jupyter.png"
535
+ podTemplate:
536
+ serviceAccount:
537
+ name: default-editor
538
+ volumeMounts:
539
+ home: "/home/jovyan"
540
+ options:
541
+ imageConfig:
542
+ default: "jupyterlab_scipy_180"
543
+ values:
544
+ - id: "jupyterlab_scipy_180"
545
+ displayName: "JupyterLab SciPy 1.8.0"
546
+ description: "JupyterLab with SciPy 1.8.0"
547
+ spec:
548
+ image: "jupyter/scipy-notebook:2024.1.0"
549
+ podConfig:
550
+ default: "tiny_cpu"
551
+ values:
552
+ - id: "tiny_cpu"
553
+ displayName: "Tiny CPU"
554
+ description: "1 CPU core, 2GB RAM"
555
+ spec:
556
+ resources:
557
+ requests:
558
+ cpu: "100m"
559
+ memory: "512Mi"
560
+ limits:
561
+ cpu: "1"
562
+ memory: "2Gi"
563
+ ` , newWorkspaceKindName ))
564
+ })
565
+
566
+ AfterEach (func () {
567
+ By ("deleting the WorkspaceKind if it exists" )
568
+ workspaceKind := & kubefloworgv1beta1.WorkspaceKind {
569
+ ObjectMeta : metav1.ObjectMeta {
570
+ Name : newWorkspaceKindName ,
571
+ },
572
+ }
573
+ _ = k8sClient .Delete (ctx , workspaceKind )
574
+ })
575
+
576
+ It ("should create a WorkspaceKind successfully without dry-run" , func () {
577
+ req , err := http .NewRequest (http .MethodPost , AllWorkspaceKindsPath , bytes .NewReader (validYAML ))
578
+ Expect (err ).NotTo (HaveOccurred ())
579
+ req .Header .Set ("Content-Type" , MediaTypeYaml )
580
+ req .Header .Set (userIdHeader , adminUser )
581
+
582
+ rr := httptest .NewRecorder ()
583
+ a .CreateWorkspaceKindHandler (rr , req , httprouter.Params {})
584
+ rs := rr .Result ()
585
+ defer rs .Body .Close ()
586
+
587
+ Expect (rs .StatusCode ).To (Equal (http .StatusCreated ), descUnexpectedHTTPStatus , rr .Body .String ())
588
+
589
+ location := rs .Header .Get ("Location" )
590
+ Expect (location ).To (Equal (fmt .Sprintf ("/api/v1/workspacekinds/%s" , newWorkspaceKindName )))
591
+
592
+ body , err := io .ReadAll (rs .Body )
593
+ Expect (err ).NotTo (HaveOccurred ())
594
+
595
+ var response WorkspaceKindCreateEnvelope
596
+ err = json .Unmarshal (body , & response )
597
+ Expect (err ).NotTo (HaveOccurred ())
598
+
599
+ created := & kubefloworgv1beta1.WorkspaceKind {}
600
+ err = k8sClient .Get (ctx , types.NamespacedName {Name : newWorkspaceKindName }, created )
601
+ Expect (err ).NotTo (HaveOccurred ())
602
+
603
+ expected := models .NewWorkspaceKindModelFromWorkspaceKind (created )
604
+ Expect (response .Data ).To (BeComparableTo (expected ))
605
+ })
606
+
607
+ It ("should validate WorkspaceKind with dry-run=true without persisting it" , func () {
608
+ req , err := http .NewRequest (http .MethodPost , AllWorkspaceKindsPath + "?dry_run=true" , bytes .NewReader (validYAML ))
609
+ Expect (err ).NotTo (HaveOccurred ())
610
+ req .Header .Set ("Content-Type" , MediaTypeYaml )
611
+ req .Header .Set (userIdHeader , adminUser )
612
+
613
+ rr := httptest .NewRecorder ()
614
+ a .CreateWorkspaceKindHandler (rr , req , httprouter.Params {})
615
+ rs := rr .Result ()
616
+ defer rs .Body .Close ()
617
+
618
+ Expect (rs .StatusCode ).To (Equal (http .StatusOK ), descUnexpectedHTTPStatus , rr .Body .String ())
619
+
620
+ // Ensure NOT persisted
621
+ notCreated := & kubefloworgv1beta1.WorkspaceKind {}
622
+ err = k8sClient .Get (ctx , types.NamespacedName {Name : newWorkspaceKindName }, notCreated )
623
+ Expect (err ).To (HaveOccurred ())
624
+ Expect (apierrors .IsNotFound (err )).To (BeTrue ())
625
+ })
626
+
627
+ It ("should return 400 for invalid YAML" , func () {
628
+ invalidYAML := []byte ("invalid: yaml: :" )
629
+
630
+ req , err := http .NewRequest (http .MethodPost , AllWorkspaceKindsPath , bytes .NewReader (invalidYAML ))
631
+ Expect (err ).NotTo (HaveOccurred ())
632
+ req .Header .Set ("Content-Type" , MediaTypeYaml )
633
+ req .Header .Set (userIdHeader , adminUser )
634
+
635
+ rr := httptest .NewRecorder ()
636
+ a .CreateWorkspaceKindHandler (rr , req , httprouter.Params {})
637
+ rs := rr .Result ()
638
+ defer rs .Body .Close ()
639
+
640
+ Expect (rs .StatusCode ).To (Equal (http .StatusBadRequest ), descUnexpectedHTTPStatus , rr .Body .String ())
641
+ })
642
+
643
+ It ("should return 415 for wrong content-type when dry-run not set" , func () {
644
+ req , err := http .NewRequest (http .MethodPost , AllWorkspaceKindsPath , bytes .NewReader (validYAML ))
645
+ Expect (err ).NotTo (HaveOccurred ())
646
+ req .Header .Set ("Content-Type" , MediaTypeJson )
647
+ req .Header .Set (userIdHeader , adminUser )
648
+
649
+ rr := httptest .NewRecorder ()
650
+ a .CreateWorkspaceKindHandler (rr , req , httprouter.Params {})
651
+ rs := rr .Result ()
652
+ defer rs .Body .Close ()
653
+
654
+ Expect (rs .StatusCode ).To (Equal (http .StatusUnsupportedMediaType ), descUnexpectedHTTPStatus , rr .Body .String ())
655
+ })
656
+
657
+ It ("should return 400 for wrong content-type when dry-run is set" , func () {
658
+ req , err := http .NewRequest (http .MethodPost , AllWorkspaceKindsPath + "?dry_run=true" , bytes .NewReader (validYAML ))
659
+ Expect (err ).NotTo (HaveOccurred ())
660
+ req .Header .Set ("Content-Type" , MediaTypeJson )
661
+ req .Header .Set (userIdHeader , adminUser )
662
+
663
+ rr := httptest .NewRecorder ()
664
+ a .CreateWorkspaceKindHandler (rr , req , httprouter.Params {})
665
+ rs := rr .Result ()
666
+ defer rs .Body .Close ()
667
+
668
+ Expect (rs .StatusCode ).To (Equal (http .StatusBadRequest ), descUnexpectedHTTPStatus , rr .Body .String ())
669
+ })
670
+
671
+ It ("should return 400 for invalid dry-run value" , func () {
672
+ req , err := http .NewRequest (http .MethodPost , AllWorkspaceKindsPath + "?dry_run=invalid" , bytes .NewReader (validYAML ))
673
+ Expect (err ).NotTo (HaveOccurred ())
674
+ req .Header .Set ("Content-Type" , MediaTypeYaml )
675
+ req .Header .Set (userIdHeader , adminUser )
676
+
677
+ rr := httptest .NewRecorder ()
678
+ a .CreateWorkspaceKindHandler (rr , req , httprouter.Params {})
679
+ rs := rr .Result ()
680
+ defer rs .Body .Close ()
681
+
682
+ Expect (rs .StatusCode ).To (Equal (http .StatusBadRequest ), descUnexpectedHTTPStatus , rr .Body .String ())
683
+ })
684
+
685
+ It ("should surface Kubernetes validation errors when dry-run=true with invalid resource" , func () {
686
+ invalidDryRunYAML := []byte (`
687
+ apiVersion: kubeflow.org/v1beta1
688
+ kind: WorkspaceKind
689
+ metadata:
690
+ name: INVALID_NAME # invalid DNS-1123 (uppercase not allowed)
691
+ spec:
692
+ spawner:
693
+ displayName: "Bad Test"
694
+ description: "Invalid name test"
695
+ ` )
696
+
697
+ req , _ := http .NewRequest (http .MethodPost , AllWorkspaceKindsPath + "?dry_run=true" , bytes .NewReader (invalidDryRunYAML ))
698
+ req .Header .Set ("Content-Type" , MediaTypeYaml )
699
+ req .Header .Set (userIdHeader , adminUser )
700
+
701
+ rr := httptest .NewRecorder ()
702
+ a .CreateWorkspaceKindHandler (rr , req , httprouter.Params {})
703
+ rs := rr .Result ()
704
+ defer rs .Body .Close ()
705
+
706
+ // Your handler maps apierrors.IsInvalid to 422
707
+ Expect (rs .StatusCode ).To (Equal (http .StatusUnprocessableEntity ), descUnexpectedHTTPStatus , rr .Body .String ())
708
+
709
+ body , _ := io .ReadAll (rs .Body )
710
+
711
+ // Unmarshal to the envelope shape your backend uses
712
+ var env ErrorEnvelope
713
+ _ = json .Unmarshal (body , & env )
714
+
715
+ // Ensure we received an error envelope
716
+ Expect (env .Error ).NotTo (BeNil (), "expected error envelope in response" )
717
+
718
+ // Be lenient about exact schema of causes: assert the raw payload mentions metadata.name
719
+ Expect (strings .Contains (string (body ), "metadata.name" )).
720
+ To (BeTrue (), "expected validation details about metadata.name in error payload" )
721
+
722
+ // Ensure object not persisted
723
+ notCreated := & kubefloworgv1beta1.WorkspaceKind {}
724
+ getErr := k8sClient .Get (ctx , types.NamespacedName {Name : "INVALID_NAME" }, notCreated )
725
+ Expect (apierrors .IsNotFound (getErr )).To (BeTrue ())
726
+ })
727
+ })
513
728
})
0 commit comments