@@ -19,9 +19,12 @@ package controller
1919import (
2020 "context"
2121 "fmt"
22+ "os"
2223 "strings"
2324
2425 "github.com/go-logr/logr"
26+ networkingv1 "istio.io/api/networking/v1"
27+ istiov1 "istio.io/client-go/pkg/apis/networking/v1"
2528 appsv1 "k8s.io/api/apps/v1"
2629 corev1 "k8s.io/api/core/v1"
2730 "k8s.io/apimachinery/pkg/api/equality"
@@ -68,6 +71,7 @@ const (
6871 stateMsgErrorGenFailureService = "Workspace failed to generate Service with error: %s"
6972 stateMsgErrorMultipleStatefulSets = "Workspace owns multiple StatefulSets: %s"
7073 stateMsgErrorMultipleServices = "Workspace owns multiple Services: %s"
74+ stateMsgErrorMultipleVirtualServices = "Workspace owns multiple VirtualServices: %s"
7175 stateMsgErrorStatefulSetWarningEvent = "Workspace StatefulSet has warning event: %s"
7276 stateMsgErrorPodUnschedulable = "Workspace Pod is unschedulable: %s"
7377 stateMsgErrorPodSchedulingGate = "Workspace Pod is waiting for scheduling gate: %s"
@@ -359,6 +363,71 @@ func (r *WorkspaceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
359363 // and implement the `spec.podTemplate.httpProxy` options
360364 //
361365
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+
362431 // fetch Pod
363432 // NOTE: the first StatefulSet Pod is always called "{statefulSetName}-0"
364433 podName := fmt .Sprintf ("%s-0" , statefulSetName )
@@ -423,6 +492,7 @@ func (r *WorkspaceReconciler) SetupWithManager(mgr ctrl.Manager, opts controller
423492 For (& kubefloworgv1beta1.Workspace {}).
424493 Owns (& appsv1.StatefulSet {}).
425494 Owns (& corev1.Service {}).
495+ Owns (& istiov1.VirtualService {}).
426496 Watches (
427497 & kubefloworgv1beta1.WorkspaceKind {},
428498 handler .EnqueueRequestsFromMapFunc (r .mapWorkspaceKindToRequest ),
@@ -881,6 +951,82 @@ func generateService(workspace *kubefloworgv1beta1.Workspace, imageConfigSpec ku
881951 return service , nil
882952}
883953
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+
8841030// generateWorkspaceStatus generates a WorkspaceStatus for a Workspace
8851031func (r * WorkspaceReconciler ) generateWorkspaceStatus (ctx context.Context , log logr.Logger , workspace * kubefloworgv1beta1.Workspace , pod * corev1.Pod , statefulSet * appsv1.StatefulSet ) (kubefloworgv1beta1.WorkspaceStatus , error ) {
8861032 // NOTE: some fields are populated before this function is called,
0 commit comments