Setup virtualservice to route traffic to relevant workspace

- Update the helper func with CopyDeepVirtualService

Signed-off-by: Harshad Reddy Nalla <hnalla@redhat.com>
This commit is contained in:
Harshad Reddy Nalla 2025-09-18 02:52:36 -04:00
parent 30d1facfbc
commit 37625e193f
No known key found for this signature in database
GPG Key ID: 262A80E41BCEEBF7
11 changed files with 282 additions and 7 deletions

View File

@ -25,6 +25,7 @@ import (
// to ensure that exec-entrypoint and run can make use of them.
_ "k8s.io/client-go/plugin/pkg/client/auth"
istiov1 "istio.io/client-go/pkg/apis/networking/v1"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
@ -50,6 +51,8 @@ var (
func init() {
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(istiov1.AddToScheme(scheme))
utilruntime.Must(kubefloworgv1beta1.AddToScheme(scheme))
// +kubebuilder:scaffold:scheme
}

View File

@ -2,6 +2,12 @@ apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- manager.yaml
configMapGenerator:
- envs:
- params.env
name: config
generatorOptions:
disableNameSuffixHash: true
images:
- name: controller
newName: ghcr.io/kubeflow/notebooks/workspace-controller

View File

@ -64,6 +64,22 @@ spec:
- --leader-elect
- --health-probe-bind-address=:8081
- --metrics-bind-address=0
env:
- name: USE_ISTIO
valueFrom:
configMapKeyRef:
name: config
key: USE_ISTIO
- name: ISTIO_GATEWAY
valueFrom:
configMapKeyRef:
name: config
key: ISTIO_GATEWAY
- name: ISTIO_HOST
valueFrom:
configMapKeyRef:
name: config
key: ISTIO_HOST
image: controller:latest
imagePullPolicy: IfNotPresent
name: manager

View File

@ -0,0 +1,4 @@
USE_ISTIO=true
ISTIO_GATEWAY=kubeflow/kubeflow-gateway
ISTIO_HOST=*
CLUSTER_DOMAIN=cluster.local

View File

@ -6,6 +6,9 @@ require (
github.com/go-logr/logr v1.4.2
github.com/onsi/ginkgo/v2 v2.19.0
github.com/onsi/gomega v1.33.1
golang.org/x/time v0.3.0
istio.io/api v1.22.8
istio.io/client-go v1.22.8
k8s.io/api v0.31.0
k8s.io/apimachinery v0.31.0
k8s.io/client-go v0.31.0
@ -56,9 +59,9 @@ require (
golang.org/x/sys v0.21.0 // indirect
golang.org/x/term v0.21.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect

View File

@ -9,8 +9,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U=
github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg=
github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
@ -155,6 +155,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw=
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@ -170,6 +172,10 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
istio.io/api v1.22.8 h1:mhkaeFJ13WZ2d6pvL9+exNeQ9UB6HX7e6m+XwO9XoYY=
istio.io/api v1.22.8/go.mod h1:S3l8LWqNYS9yT+d4bH+jqzH2lMencPkW7SKM1Cu9EyM=
istio.io/client-go v1.22.8 h1:wojmt220jSbfhpRDsPiflj2nSFTBuYtZNiW9hqKeaWE=
istio.io/client-go v1.22.8/go.mod h1:noO8SoyMxLwni3w+yGK67aydi2klExjmiqnXyeRS/00=
k8s.io/api v0.31.0 h1:b9LiSjR2ym/SzTOlfMHm1tr7/21aD7fSkqgD/CVJBCo=
k8s.io/api v0.31.0/go.mod h1:0YiFF+JfFxMM6+1hQei8FY8M7s1Mth+z/q7eF1aJkTE=
k8s.io/apiextensions-apiserver v0.31.0 h1:fZgCVhGwsclj3qCw1buVXCV6khjRzKC5eCFt24kyLSk=

View File

@ -42,6 +42,9 @@ import (
"sigs.k8s.io/controller-runtime/pkg/log/zap"
kubefloworgv1beta1 "github.com/kubeflow/notebooks/workspaces/controller/api/v1beta1"
istiov1 "istio.io/client-go/pkg/apis/networking/v1"
"github.com/kubeflow/notebooks/workspaces/controller/internal/helper"
// +kubebuilder:scaffold:imports
)
@ -88,6 +91,8 @@ var _ = BeforeSuite(func() {
By("setting up the scheme")
err = kubefloworgv1beta1.AddToScheme(scheme.Scheme)
Expect(err).NotTo(HaveOccurred())
err = istiov1.AddToScheme(scheme.Scheme)
Expect(err).NotTo(HaveOccurred())
// +kubebuilder:scaffold:scheme

View File

@ -19,9 +19,12 @@ package controller
import (
"context"
"fmt"
"os"
"strings"
"github.com/go-logr/logr"
networkingv1 "istio.io/api/networking/v1"
istiov1 "istio.io/client-go/pkg/apis/networking/v1"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/equality"
@ -68,6 +71,7 @@ const (
stateMsgErrorGenFailureService = "Workspace failed to generate Service with error: %s"
stateMsgErrorMultipleStatefulSets = "Workspace owns multiple StatefulSets: %s"
stateMsgErrorMultipleServices = "Workspace owns multiple Services: %s"
stateMsgErrorMultipleVirtualServices = "Workspace owns multiple VirtualServices: %s"
stateMsgErrorStatefulSetWarningEvent = "Workspace StatefulSet has warning event: %s"
stateMsgErrorPodUnschedulable = "Workspace Pod is unschedulable: %s"
stateMsgErrorPodSchedulingGate = "Workspace Pod is waiting for scheduling gate: %s"
@ -359,6 +363,76 @@ func (r *WorkspaceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
// and implement the `spec.podTemplate.httpProxy` options
//
log.V(2).Info("reconciling VirtualService for Workspace")
if os.Getenv("USE_ISTIO") == "true" {
// generateVirtualService
currentPodTemplatePortsMap := make(map[kubefloworgv1beta1.PortId]kubefloworgv1beta1.WorkspaceKindPort)
for _, port := range workspaceKind.Spec.PodTemplate.Ports {
currentPodTemplatePortsMap[port.Id] = port
}
virtualsvc, err := generateVirtualService(workspace, currentPodTemplatePortsMap, serviceName, currentImageConfig.Spec)
if err != nil {
log.V(0).Info("failed to generate VirtualService for Workspace", "error", err.Error())
return r.updateWorkspaceState(ctx, log, workspace,
kubefloworgv1beta1.WorkspaceStateError,
fmt.Sprintf("failed to generate VirtualService for Workspace: %s", err.Error()),
)
}
if err := ctrl.SetControllerReference(workspace, virtualsvc, r.Scheme); err != nil {
log.Error(err, "unable to set controller reference on VirtualService")
return ctrl.Result{}, err
}
// fetch VirtualServices
// NOTE: we filter by VirtualServices that are owned by the Workspace, not by name
// this allows us to generate a random name for the VirtualService with `metadata.generateName`
var VirtualServiceName string
ownedVirtualServices := &istiov1.VirtualServiceList{}
listOpts = &client.ListOptions{
FieldSelector: fields.OneTermEqualSelector(helper.IndexWorkspaceOwnerField, workspace.Name),
Namespace: req.Namespace,
}
if err := r.List(ctx, ownedVirtualServices, listOpts); err != nil {
log.Error(err, "unable to list VirtualServices")
return ctrl.Result{}, err
}
switch numVirtualServices := len(ownedVirtualServices.Items); {
case numVirtualServices > 1:
virtualServiceList := make([]string, len(ownedVirtualServices.Items))
for i, vs := range ownedVirtualServices.Items {
virtualServiceList[i] = vs.Name
}
virtualServiceListString := strings.Join(virtualServiceList, ", ")
log.Error(nil, "Workspace owns multiple VirtualServices", "virtualServices", virtualServiceListString)
return r.updateWorkspaceState(ctx, log, workspace,
kubefloworgv1beta1.WorkspaceStateError,
fmt.Sprintf(stateMsgErrorMultipleVirtualServices, virtualServiceListString),
)
case numVirtualServices == 0:
if err := r.Create(ctx, virtualsvc); err != nil {
log.Error(err, "unable to create VirtualService")
return ctrl.Result{}, err
}
VirtualServiceName = virtualsvc.ObjectMeta.Name
log.V(2).Info("VirtualService created", "virtualService", VirtualServiceName)
default:
foundVirtualService := ownedVirtualServices.Items[0]
VirtualServiceName = foundVirtualService.ObjectMeta.Name
if helper.CopyVirtualServiceFields(virtualsvc, foundVirtualService) {
if err := r.Update(ctx, foundVirtualService); err != nil {
if apierrors.IsConflict(err) {
log.V(2).Info("update conflict while updating VirtualService, will requeue")
return ctrl.Result{Requeue: true}, nil
}
log.Error(err, "unable to update VirtualService")
return ctrl.Result{}, err
}
log.V(2).Info("VirtualService updated", "virtualService", VirtualServiceName)
}
}
}
// fetch Pod
// NOTE: the first StatefulSet Pod is always called "{statefulSetName}-0"
podName := fmt.Sprintf("%s-0", statefulSetName)
@ -418,11 +492,20 @@ func (r *WorkspaceReconciler) SetupWithManager(mgr ctrl.Manager, opts controller
return labelExists
})
return ctrl.NewControllerManagedBy(mgr).
// Build the controller with core resources
controllerBuilder := ctrl.NewControllerManagedBy(mgr).
WithOptions(opts).
For(&kubefloworgv1beta1.Workspace{}).
Owns(&appsv1.StatefulSet{}).
Owns(&corev1.Service{}).
Owns(&corev1.Service{})
// Conditionally add VirtualService ownership if the CRD is available
// This prevents test failures when Istio CRDs are not installed
if _, err := mgr.GetRESTMapper().RESTMapping(istiov1.SchemeGroupVersion.WithKind("VirtualService").GroupKind()); err == nil {
controllerBuilder = controllerBuilder.Owns(&istiov1.VirtualService{})
}
return controllerBuilder.
Watches(
&kubefloworgv1beta1.WorkspaceKind{},
handler.EnqueueRequestsFromMapFunc(r.mapWorkspaceKindToRequest),
@ -881,6 +964,93 @@ func generateService(workspace *kubefloworgv1beta1.Workspace, imageConfigSpec ku
return service, nil
}
// generateVirtualService generates a VirtualService for a Workspace
func generateVirtualService(workspace *kubefloworgv1beta1.Workspace, currentPodTemplatePortsMap map[kubefloworgv1beta1.PortId]kubefloworgv1beta1.WorkspaceKindPort, serviceName string, imageConfigSpec kubefloworgv1beta1.ImageConfigSpec) (*istiov1.VirtualService, error) {
// NOTE: the name prefix is used to generate a unique name for the VirtualService
namePrefix := generateNamePrefix(workspace.Name, maxServiceNameLength)
// TODO: Add a possible default for istioGateway
istioGateway := os.Getenv("ISTIO_GATEWAY")
if istioGateway == "" {
return nil, fmt.Errorf("ISTIO_GATEWAY environment variable is not set")
}
istioHosts := "*"
if istioHostsEnv, ok := os.LookupEnv("ISTIO_HOSTS"); ok {
istioHosts = istioHostsEnv
}
clusterDomain := "cluster.local"
if clusterDomainEnv, ok := os.LookupEnv("CLUSTER_DOMAIN"); ok {
clusterDomain = clusterDomainEnv
}
serviceHost := fmt.Sprintf("%s.%s.svc.%s", serviceName, workspace.Namespace, clusterDomain)
virtualService := &istiov1.VirtualService{
ObjectMeta: metav1.ObjectMeta{
GenerateName: namePrefix,
Namespace: workspace.Namespace,
Labels: map[string]string{
workspaceNameLabel: workspace.Name,
},
},
Spec: networkingv1.VirtualService{
Gateways: []string{istioGateway},
Hosts: []string{istioHosts},
},
}
for _, port := range imageConfigSpec.Ports {
if _, exists := currentPodTemplatePortsMap[port.Id]; exists {
podTemplatePort := currentPodTemplatePortsMap[port.Id]
matchUriPrefix := fmt.Sprintf("/workspaces/connect/%s/%s/%s/", workspace.Namespace, workspace.Name, port.Id)
// Additional Cases would be added for SSH, etc.
switch podTemplatePort.Protocol { //nolint:gocritic
case kubefloworgv1beta1.ImagePortProtocolHTTP:
virtualServiceSpecHttp := &networkingv1.HTTPRoute{
Headers: &networkingv1.Headers{
Request: &networkingv1.Headers_HeaderOperations{},
},
Match: []*networkingv1.HTTPMatchRequest{
{
Uri: &networkingv1.StringMatch{
MatchType: &networkingv1.StringMatch_Prefix{
Prefix: matchUriPrefix,
},
},
},
},
Route: []*networkingv1.HTTPRouteDestination{
{
Destination: &networkingv1.Destination{
Host: serviceHost,
Port: &networkingv1.PortSelector{
Number: uint32(port.Port), //nolint:gosec
},
},
},
},
}
if !*podTemplatePort.HTTPProxy.RemovePathPrefix {
virtualServiceSpecHttp.Rewrite.Uri = fmt.Sprintf("/workspaces/connect/%s/%s/%s/", workspace.Namespace, workspace.Name, port.Id)
}
if podTemplatePort.HTTPProxy.RequestHeaders.Set != nil {
virtualServiceSpecHttp.Headers.Request.Set = podTemplatePort.HTTPProxy.RequestHeaders.Set
}
if podTemplatePort.HTTPProxy.RequestHeaders.Add != nil {
virtualServiceSpecHttp.Headers.Request.Add = podTemplatePort.HTTPProxy.RequestHeaders.Add
}
if podTemplatePort.HTTPProxy.RequestHeaders.Remove != nil {
virtualServiceSpecHttp.Headers.Request.Remove = podTemplatePort.HTTPProxy.RequestHeaders.Remove
}
virtualService.Spec.Http = append(virtualService.Spec.Http, virtualServiceSpecHttp)
}
}
}
return virtualService, nil
}
// generateWorkspaceStatus generates a WorkspaceStatus for a Workspace
func (r *WorkspaceReconciler) generateWorkspaceStatus(ctx context.Context, log logr.Logger, workspace *kubefloworgv1beta1.Workspace, pod *corev1.Pod, statefulSet *appsv1.StatefulSet) (kubefloworgv1beta1.WorkspaceStatus, error) {
// NOTE: some fields are populated before this function is called,

View File

@ -19,6 +19,7 @@ package helper
import (
"reflect"
istiov1 "istio.io/client-go/pkg/apis/networking/v1"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
@ -118,6 +119,44 @@ func CopyServiceFields(desired *corev1.Service, target *corev1.Service) bool {
return requireUpdate
}
// CopyVirtualServiceFields updates a target VirtualService with the fields from a desired VirtualService, returning true if an update is required.
func CopyVirtualServiceFields(desired *istiov1.VirtualService, target *istiov1.VirtualService) bool {
requireUpdate := false
// Using the Spec definition https://pkg.go.dev/istio.io/api/networking/v1alpha3alpha3#VirtualService
if !reflect.DeepEqual(target.Spec.Gateways, desired.Spec.Gateways) {
target.Spec.Gateways = desired.Spec.Gateways
requireUpdate = true
}
if !reflect.DeepEqual(target.Spec.Hosts, desired.Spec.Hosts) {
target.Spec.Hosts = desired.Spec.Hosts
requireUpdate = true
}
if !reflect.DeepEqual(target.Spec.Http, desired.Spec.Http) {
target.Spec.Http = desired.Spec.Http
requireUpdate = true
}
if !reflect.DeepEqual(target.Spec.Tls, desired.Spec.Tls) {
target.Spec.Tls = desired.Spec.Tls
requireUpdate = true
}
if !reflect.DeepEqual(target.Spec.Tcp, desired.Spec.Tcp) {
target.Spec.Tcp = desired.Spec.Tcp
requireUpdate = true
}
if !reflect.DeepEqual(target.Spec.ExportTo, desired.Spec.ExportTo) {
target.Spec.ExportTo = desired.Spec.ExportTo
requireUpdate = true
}
return requireUpdate
}
// NormalizePodConfigSpec normalizes a PodConfigSpec so that it can be compared with reflect.DeepEqual
func NormalizePodConfigSpec(spec kubefloworgv1beta1.PodConfigSpec) error {

View File

@ -19,6 +19,7 @@ package helper
import (
"context"
istiov1 "istio.io/client-go/pkg/apis/networking/v1"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -32,6 +33,7 @@ const (
IndexEventInvolvedObjectUidField = ".involvedObject.uid"
IndexWorkspaceOwnerField = ".metadata.controller"
IndexWorkspaceKindField = ".spec.kind"
OwnerKindWorkspace = "Workspace"
)
// SetupManagerFieldIndexers sets up field indexes on a controller-runtime manager
@ -55,7 +57,7 @@ func SetupManagerFieldIndexers(mgr ctrl.Manager) error {
if owner == nil {
return nil
}
if owner.APIVersion != kubefloworgv1beta1.GroupVersion.String() || owner.Kind != "Workspace" {
if owner.APIVersion != kubefloworgv1beta1.GroupVersion.String() || owner.Kind != OwnerKindWorkspace {
return nil
}
return []string{owner.Name}
@ -70,7 +72,7 @@ func SetupManagerFieldIndexers(mgr ctrl.Manager) error {
if owner == nil {
return nil
}
if owner.APIVersion != kubefloworgv1beta1.GroupVersion.String() || owner.Kind != "Workspace" {
if owner.APIVersion != kubefloworgv1beta1.GroupVersion.String() || owner.Kind != OwnerKindWorkspace {
return nil
}
return []string{owner.Name}
@ -78,6 +80,23 @@ func SetupManagerFieldIndexers(mgr ctrl.Manager) error {
return err
}
// Index VirtualService by its owner Workspace
// This is conditional and will not fail if VirtualService CRD is not available (e.g., in test environments)
if err := mgr.GetFieldIndexer().IndexField(context.Background(), &istiov1.VirtualService{}, IndexWorkspaceOwnerField, func(rawObj client.Object) []string {
virtualService := rawObj.(*istiov1.VirtualService)
owner := metav1.GetControllerOf(virtualService)
if owner == nil {
return nil
}
if owner.APIVersion != kubefloworgv1beta1.GroupVersion.String() || owner.Kind != OwnerKindWorkspace {
return nil
}
return []string{owner.Name}
}); err != nil {
// Log the error but don't fail if VirtualService CRD is not available (e.g., in test environments)
ctrl.Log.Info("Failed to setup VirtualService field indexer, VirtualService CRD may not be available", "error", err)
}
// Index Workspace by WorkspaceKind
if err := mgr.GetFieldIndexer().IndexField(context.Background(), &kubefloworgv1beta1.Workspace{}, IndexWorkspaceKindField, func(rawObj client.Object) []string {
ws := rawObj.(*kubefloworgv1beta1.Workspace)

View File

@ -46,6 +46,8 @@ import (
kubefloworgv1beta1 "github.com/kubeflow/notebooks/workspaces/controller/api/v1beta1"
"github.com/kubeflow/notebooks/workspaces/controller/internal/helper"
istiov1 "istio.io/client-go/pkg/apis/networking/v1"
// +kubebuilder:scaffold:imports
)
@ -95,6 +97,8 @@ var _ = BeforeSuite(func() {
By("setting up the scheme")
err = kubefloworgv1beta1.AddToScheme(scheme.Scheme)
Expect(err).NotTo(HaveOccurred())
err = istiov1.AddToScheme(scheme.Scheme)
Expect(err).NotTo(HaveOccurred())
// +kubebuilder:scaffold:scheme