Skip to content

Commit 6764331

Browse files
committed
Add call to reconcile VirtualService
1 parent bc4e445 commit 6764331

File tree

4 files changed

+16699
-1
lines changed

4 files changed

+16699
-1
lines changed

workspaces/controller/KIND.md

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Running Notebooks Workspace with Kind
2+
3+
4+
## Create KinD cluster
5+
6+
```shell
7+
kind create cluster
8+
```
9+
10+
## Install CertManager
11+
12+
```shell
13+
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.15.3/cert-manager.yaml
14+
```
15+
16+
Check if CertManager pods are running
17+
18+
```shell
19+
kubectl get pods --namespace cert-manager
20+
```
21+
22+
The output should looks something like it:
23+
24+
```shell
25+
NAME READY STATUS RESTARTS AGE
26+
cert-manager-7fbbc65b49-x62l8 1/1 Running 0 4m49s
27+
cert-manager-cainjector-6664fc84f6-rckz5 1/1 Running 0 4m49s
28+
cert-manager-webhook-59598898fd-sq87w 1/1 Running 0 4m49s
29+
```
30+
31+
## Create jupyterlab namespace
32+
33+
Create a namespace called `jupyterlab`
34+
35+
```shell
36+
kubectl create namespace jupyterlab
37+
```

workspaces/controller/internal/controller/suite_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ var _ = BeforeSuite(func() {
7070

7171
By("bootstrapping test environment")
7272
testEnv = &envtest.Environment{
73-
CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")},
73+
CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases"), filepath.Join("..", "..", "test", "crd")},
7474
ErrorIfCRDPathMissing: true,
7575

7676
// The BinaryAssetsDirectory is only required if you want to run the tests directly without call the makefile target test.

workspaces/controller/internal/controller/workspace_controller.go

+136
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ package controller
1919
import (
2020
"context"
2121
"fmt"
22+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
23+
"os"
2224
"reflect"
2325
"strings"
2426

@@ -346,6 +348,19 @@ func (r *WorkspaceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
346348
// TODO: reconcile the Istio VirtualService to expose the Workspace
347349
// and implement the `spec.podTemplate.httpProxy` options
348350
//
351+
virtualService, err := GenerateIstioVirtualService(workspace, workspaceKind, currentImageConfig, serviceName)
352+
if err != nil {
353+
log.Error(err, "unable to generate Istio Virtual Service")
354+
}
355+
log.Info(fmt.Sprintf("VirtualService %s", virtualService))
356+
357+
if err := ctrl.SetControllerReference(workspace, virtualService, r.Scheme); err != nil {
358+
return ctrl.Result{}, err
359+
}
360+
361+
if err := ReconcileVirtualService(ctx, r.Client, virtualService.GetName(), virtualService.GetNamespace(), virtualService, log); err != nil {
362+
return ctrl.Result{}, err
363+
}
349364

350365
// fetch Pod
351366
// NOTE: the first StatefulSet Pod is always called "{statefulSetName}-0"
@@ -1003,3 +1018,124 @@ func (r *WorkspaceReconciler) generateWorkspaceStatus(ctx context.Context, log l
10031018
status.StateMessage = stateMsgUnknown
10041019
return status, nil
10051020
}
1021+
1022+
const istioApiVersion = "networking.istio.io/v1"
1023+
const virtualServiceKind = "VirtualService"
1024+
1025+
func GenerateIstioVirtualService(workspace *kubefloworgv1beta1.Workspace, workspaceKind *kubefloworgv1beta1.WorkspaceKind, imageConfig *kubefloworgv1beta1.ImageConfigValue, serviceName string) (*unstructured.Unstructured, error) {
1026+
1027+
virtualService := &unstructured.Unstructured{}
1028+
virtualService.SetAPIVersion(istioApiVersion)
1029+
virtualService.SetKind(virtualServiceKind)
1030+
1031+
prefix := generateNamePrefix(workspace.Name, maxServiceNameLength)
1032+
virtualService.SetName(removeTrailingDash(prefix))
1033+
virtualService.SetNamespace(workspace.Namespace)
1034+
1035+
// .spec.gateways
1036+
istioGateway := getEnvOrDefault("ISTIO_GATEWAY", "kubeflow/kubeflow-gateway")
1037+
if err := unstructured.SetNestedStringSlice(virtualService.Object, []string{istioGateway},
1038+
"spec", "gateways"); err != nil {
1039+
return nil, fmt.Errorf("set .spec.gateways error: %v", err)
1040+
}
1041+
1042+
istioHost := getEnvOrDefault("ISTIO_HOST", "*")
1043+
if err := unstructured.SetNestedStringSlice(virtualService.Object, []string{istioHost},
1044+
"spec", "gateways"); err != nil {
1045+
return nil, fmt.Errorf("set .spec.hosts error: %v", err)
1046+
}
1047+
1048+
var prefixes []string
1049+
for _, imagePort := range imageConfig.Spec.Ports {
1050+
prefix := fmt.Sprintf("/workspace/%s/%s/%s", workspace.Namespace, workspace.Name, imagePort.Id)
1051+
prefixes = append(prefixes, prefix)
1052+
}
1053+
1054+
var httpRoutes []interface{}
1055+
1056+
_ = fmt.Sprintf("%s.%s.svc.%s", workspace.Name, workspace.Namespace, getEnvOrDefault("CLUSTER_DOMAIN", "cluster.local"))
1057+
for _, imagePort := range imageConfig.Spec.Ports {
1058+
1059+
httpRoute := map[string]interface{}{
1060+
"match": []map[string]interface{}{
1061+
{
1062+
"uri": map[string]interface{}{
1063+
"prefix": fmt.Sprintf("/workspace/%s/%s/%s", workspace.Namespace, workspace.Name, imagePort.Id),
1064+
},
1065+
},
1066+
},
1067+
"route": []map[string]interface{}{
1068+
{
1069+
"destination": map[string]interface{}{
1070+
"host": fmt.Sprintf("%s.%s.svc.%s", serviceName, workspace.Namespace, getEnvOrDefault("CLUSTER_DOMAIN", "cluster.local")),
1071+
"port": map[string]interface{}{
1072+
"number": imagePort.Port,
1073+
},
1074+
},
1075+
},
1076+
},
1077+
}
1078+
1079+
if *workspaceKind.Spec.PodTemplate.HTTPProxy.RemovePathPrefix {
1080+
httpRoute["rewrite"] = map[string]interface{}{"uri": "/"}
1081+
}
1082+
1083+
httpRoutes = append(httpRoutes, httpRoute)
1084+
}
1085+
1086+
virtualService.Object["spec"] = map[string]interface{}{
1087+
"gateways": []string{
1088+
istioGateway,
1089+
},
1090+
"hosts": []string{
1091+
istioHost,
1092+
},
1093+
"http": httpRoutes,
1094+
}
1095+
1096+
return virtualService, nil
1097+
}
1098+
1099+
func getEnvOrDefault(name, defaultValue string) string {
1100+
if lookupEnv, exists := os.LookupEnv(name); exists {
1101+
return lookupEnv
1102+
} else {
1103+
return defaultValue
1104+
}
1105+
}
1106+
1107+
func ReconcileVirtualService(ctx context.Context, r client.Client, virtualServiceName, namespace string, virtualService *unstructured.Unstructured, log logr.Logger) error {
1108+
foundVirtualService := &unstructured.Unstructured{}
1109+
foundVirtualService.SetAPIVersion(istioApiVersion)
1110+
foundVirtualService.SetKind(virtualServiceKind)
1111+
justCreated := false
1112+
if err := r.Get(ctx, types.NamespacedName{Name: virtualServiceName, Namespace: namespace}, foundVirtualService); err != nil {
1113+
if apierrors.IsNotFound(err) {
1114+
log.Info("Creating virtual service", "namespace", namespace, "name", virtualServiceName)
1115+
if err := r.Create(ctx, virtualService); err != nil {
1116+
log.Error(err, "unable to create virtual service")
1117+
return err
1118+
}
1119+
justCreated = true
1120+
} else {
1121+
log.Error(err, "error getting virtual service")
1122+
return err
1123+
}
1124+
}
1125+
if !justCreated { // TODO: we need to evict unnecessary update
1126+
log.Info("Updating virtual service", "namespace", namespace, "name", virtualServiceName)
1127+
if err := r.Update(ctx, foundVirtualService); err != nil {
1128+
log.Error(err, "unable to update virtual service")
1129+
return err
1130+
}
1131+
}
1132+
1133+
return nil
1134+
}
1135+
1136+
func removeTrailingDash(s string) string {
1137+
if len(s) > 0 && s[len(s)-1] == '-' {
1138+
return s[:len(s)-1]
1139+
}
1140+
return s
1141+
}

0 commit comments

Comments
 (0)