@@ -19,9 +19,12 @@ package controller
19
19
import (
20
20
"context"
21
21
"fmt"
22
+ "os"
22
23
"strings"
23
24
24
25
"github.com/go-logr/logr"
26
+ networkingv1 "istio.io/api/networking/v1"
27
+ istiov1 "istio.io/client-go/pkg/apis/networking/v1"
25
28
appsv1 "k8s.io/api/apps/v1"
26
29
corev1 "k8s.io/api/core/v1"
27
30
"k8s.io/apimachinery/pkg/api/equality"
@@ -68,6 +71,7 @@ const (
68
71
stateMsgErrorGenFailureService = "Workspace failed to generate Service with error: %s"
69
72
stateMsgErrorMultipleStatefulSets = "Workspace owns multiple StatefulSets: %s"
70
73
stateMsgErrorMultipleServices = "Workspace owns multiple Services: %s"
74
+ stateMsgErrorMultipleVirtualServices = "Workspace owns multiple VirtualServices: %s"
71
75
stateMsgErrorStatefulSetWarningEvent = "Workspace StatefulSet has warning event: %s"
72
76
stateMsgErrorPodUnschedulable = "Workspace Pod is unschedulable: %s"
73
77
stateMsgErrorPodSchedulingGate = "Workspace Pod is waiting for scheduling gate: %s"
@@ -359,6 +363,71 @@ func (r *WorkspaceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
359
363
// and implement the `spec.podTemplate.httpProxy` options
360
364
//
361
365
366
+ log .V (2 ).Info ("reconciling VirtualService for Workspace" )
367
+ if os .Getenv ("USE_ISTIO" ) == "true" {
368
+ // generateVirtualService
369
+ virtualsvc , err := generateVirtualService (workspace , serviceName , currentImageConfig .Spec )
370
+ if err != nil {
371
+ log .V (0 ).Info ("failed to generate VirtualService for Workspace" , "error" , err .Error ())
372
+ return r .updateWorkspaceState (ctx , log , workspace ,
373
+ kubefloworgv1beta1 .WorkspaceStateError ,
374
+ fmt .Sprintf ("failed to generate VirtualService for Workspace: %s" , err .Error ()),
375
+ )
376
+ }
377
+ if err := ctrl .SetControllerReference (workspace , virtualsvc , r .Scheme ); err != nil {
378
+ log .Error (err , "unable to set controller reference on VirtualService" )
379
+ return ctrl.Result {}, err
380
+ }
381
+
382
+ // fetch VirtualServices
383
+ // NOTE: we filter by VirtualServices that are owned by the Workspace, not by name
384
+ // this allows us to generate a random name for the VirtualService with `metadata.generateName`
385
+ var VirtualServiceName string
386
+ ownedVirtualServices := & istiov1.VirtualServiceList {}
387
+ listOpts = & client.ListOptions {
388
+ FieldSelector : fields .OneTermEqualSelector (helper .IndexWorkspaceOwnerField , workspace .Name ),
389
+ Namespace : req .Namespace ,
390
+ }
391
+ if err := r .List (ctx , ownedVirtualServices , listOpts ); err != nil {
392
+ log .Error (err , "unable to list VirtualServices" )
393
+ return ctrl.Result {}, err
394
+ }
395
+ switch numVirtualServices := len (ownedVirtualServices .Items ); {
396
+ case numVirtualServices > 1 :
397
+ virtualServiceList := make ([]string , len (ownedVirtualServices .Items ))
398
+ for i , vs := range ownedVirtualServices .Items {
399
+ virtualServiceList [i ] = vs .Name
400
+ }
401
+ virtualServiceListString := strings .Join (virtualServiceList , ", " )
402
+ log .Error (nil , "Workspace owns multiple VirtualServices" , "virtualServices" , virtualServiceListString )
403
+ return r .updateWorkspaceState (ctx , log , workspace ,
404
+ kubefloworgv1beta1 .WorkspaceStateError ,
405
+ fmt .Sprintf (stateMsgErrorMultipleVirtualServices , virtualServiceListString ),
406
+ )
407
+ case numVirtualServices == 0 :
408
+ if err := r .Create (ctx , virtualsvc ); err != nil {
409
+ log .Error (err , "unable to create VirtualService" )
410
+ return ctrl.Result {}, err
411
+ }
412
+ VirtualServiceName = virtualsvc .ObjectMeta .Name
413
+ log .V (2 ).Info ("VirtualService created" , "virtualService" , VirtualServiceName )
414
+ default :
415
+ foundVirtualService := ownedVirtualServices .Items [0 ]
416
+ VirtualServiceName = foundVirtualService .ObjectMeta .Name
417
+ if helper .CopyVirtualServiceFields (virtualsvc , foundVirtualService ) {
418
+ if err := r .Update (ctx , foundVirtualService ); err != nil {
419
+ if apierrors .IsConflict (err ) {
420
+ log .V (2 ).Info ("update conflict while updating VirtualService, will requeue" )
421
+ return ctrl.Result {Requeue : true }, nil
422
+ }
423
+ log .Error (err , "unable to update VirtualService" )
424
+ return ctrl.Result {}, err
425
+ }
426
+ log .V (2 ).Info ("VirtualService updated" , "virtualService" , VirtualServiceName )
427
+ }
428
+ }
429
+ }
430
+
362
431
// fetch Pod
363
432
// NOTE: the first StatefulSet Pod is always called "{statefulSetName}-0"
364
433
podName := fmt .Sprintf ("%s-0" , statefulSetName )
@@ -423,6 +492,7 @@ func (r *WorkspaceReconciler) SetupWithManager(mgr ctrl.Manager, opts controller
423
492
For (& kubefloworgv1beta1.Workspace {}).
424
493
Owns (& appsv1.StatefulSet {}).
425
494
Owns (& corev1.Service {}).
495
+ Owns (& istiov1.VirtualService {}).
426
496
Watches (
427
497
& kubefloworgv1beta1.WorkspaceKind {},
428
498
handler .EnqueueRequestsFromMapFunc (r .mapWorkspaceKindToRequest ),
@@ -881,6 +951,82 @@ func generateService(workspace *kubefloworgv1beta1.Workspace, imageConfigSpec ku
881
951
return service , nil
882
952
}
883
953
954
+ // generateVirtualService generates a VirtualService for a Workspace
955
+ func generateVirtualService (workspace * kubefloworgv1beta1.Workspace , serviceName string , imageConfigSpec kubefloworgv1beta1.ImageConfigSpec ) (* istiov1.VirtualService , error ) {
956
+ // NOTE: the name prefix is used to generate a unique name for the VirtualService
957
+ namePrefix := generateNamePrefix (workspace .Name , maxServiceNameLength )
958
+
959
+ // TODO: Change this to reference podtemplate ports.[].portID
960
+ portID := imageConfigSpec .Ports [0 ].Id
961
+ matchUriPrefix := fmt .Sprintf ("/workspace/%s/%s/%s/" , workspace .Namespace , workspace .Name , portID )
962
+
963
+ // TODO: Change this to reference podtemplate ports.[].httpProxy.removePathPrefix
964
+ rewriteUri := fmt .Sprintf ("/workspace/%s/%s/" , workspace .Namespace , workspace .Name )
965
+
966
+ clusterDomain := "cluster.local"
967
+ if clusterDomainEnv , ok := os .LookupEnv ("CLUSTER_DOMAIN" ); ok {
968
+ clusterDomain = clusterDomainEnv
969
+ }
970
+ serviceHost := fmt .Sprintf ("%s.%s.svc.%s" , serviceName , workspace .Namespace , clusterDomain )
971
+
972
+ // TODO: Add a possible default for istioGateway
973
+ istioGateway := os .Getenv ("ISTIO_GATEWAY" )
974
+ istioHosts := "*"
975
+ if istioHostsEnv , ok := os .LookupEnv ("ISTIO_HOSTS" ); ok {
976
+ istioHosts = istioHostsEnv
977
+ }
978
+
979
+ // generate VirtualService
980
+ virtualService := & istiov1.VirtualService {
981
+ ObjectMeta : metav1.ObjectMeta {
982
+ GenerateName : namePrefix ,
983
+ Namespace : workspace .Namespace ,
984
+ Labels : map [string ]string {
985
+ workspaceNameLabel : workspace .Name ,
986
+ },
987
+ },
988
+ Spec : networkingv1.VirtualService {
989
+ Gateways : []string {istioGateway },
990
+ Hosts : []string {istioHosts },
991
+ Http : []* networkingv1.HTTPRoute {
992
+ {
993
+ Headers : & networkingv1.Headers {
994
+ Request : & networkingv1.Headers_HeaderOperations {},
995
+ },
996
+ Match : []* networkingv1.HTTPMatchRequest {
997
+ {
998
+ Uri : & networkingv1.StringMatch {
999
+ MatchType : & networkingv1.StringMatch_Prefix {
1000
+ Prefix : matchUriPrefix ,
1001
+ },
1002
+ },
1003
+ },
1004
+ },
1005
+ Route : []* networkingv1.HTTPRouteDestination {
1006
+ {
1007
+ Destination : & networkingv1.Destination {
1008
+ Host : serviceHost ,
1009
+ Port : & networkingv1.PortSelector {
1010
+ Number : uint32 (imageConfigSpec .Ports [0 ].Port ), // use the first port as the destination port
1011
+ },
1012
+ },
1013
+ },
1014
+ },
1015
+ },
1016
+ },
1017
+ },
1018
+ }
1019
+
1020
+ // set the rewrite URI if it is not empty
1021
+ if rewriteUri != "" {
1022
+ virtualService .Spec .Http [0 ].Rewrite = & networkingv1.HTTPRewrite {
1023
+ Uri : rewriteUri ,
1024
+ }
1025
+ }
1026
+
1027
+ return virtualService , nil
1028
+ }
1029
+
884
1030
// generateWorkspaceStatus generates a WorkspaceStatus for a Workspace
885
1031
func (r * WorkspaceReconciler ) generateWorkspaceStatus (ctx context.Context , log logr.Logger , workspace * kubefloworgv1beta1.Workspace , pod * corev1.Pod , statefulSet * appsv1.StatefulSet ) (kubefloworgv1beta1.WorkspaceStatus , error ) {
886
1032
// NOTE: some fields are populated before this function is called,
0 commit comments