Skip to content

Commit ddd093a

Browse files
authored
Support for VirtualServices for InferenceLogger traffic (#332)
* Generate KServe Inference Logger in conformance with DestinationRule and VirtualService * Add VirtualService creation for models in the mesh * Add permissions for VirtualServices * Update manifests for VirtualServices * Fix VirtualServiceName variable
1 parent fb54647 commit ddd093a

File tree

6 files changed

+143
-1
lines changed

6 files changed

+143
-1
lines changed

config/rbac/role.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,18 @@ rules:
154154
- patch
155155
- update
156156
- watch
157+
- apiGroups:
158+
- networking.istio.io
159+
resources:
160+
- virtualservices
161+
verbs:
162+
- create
163+
- delete
164+
- get
165+
- list
166+
- patch
167+
- update
168+
- watch
157169
- apiGroups:
158170
- rbac.authorization.k8s.io
159171
resources:

controllers/inference_services.go

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ func (r *TrustyAIServiceReconciler) handleInferenceServices(ctx context.Context,
241241
// patchKServe adds a TrustyAI service as an InferenceLogger to a KServe InferenceService
242242
func (r *TrustyAIServiceReconciler) patchKServe(ctx context.Context, instance *trustyaiopendatahubiov1alpha1.TrustyAIService, infService kservev1beta1.InferenceService, namespace string, crName string, remove bool) error {
243243

244-
url := generateNonTLSServiceURL(crName, namespace)
244+
url := generateKServeLoggerURL(crName, namespace)
245245

246246
if remove {
247247
if infService.Spec.Predictor.Logger == nil || *infService.Spec.Predictor.Logger.URL != url {
@@ -295,6 +295,25 @@ func (r *TrustyAIServiceReconciler) patchKServe(ctx context.Context, instance *t
295295
log.FromContext(ctx).Error(err, "InferenceService has service mesh annotation but DestinationRule CRD not found")
296296
}
297297

298+
// Check if VirtualService CRD is present. If there's an error, don't proceed and return the error
299+
exists, err = r.isVirtualServiceCRDPresent(ctx)
300+
if err != nil {
301+
log.FromContext(ctx).Error(err, "Error verifying VirtualService CRD is present")
302+
return err
303+
}
304+
305+
// Try to create the VirtualService, since CRD exists
306+
if exists {
307+
err := r.ensureVirtualService(ctx, instance)
308+
if err != nil {
309+
return fmt.Errorf("failed to ensure VirtualService: %v", err)
310+
}
311+
} else {
312+
// VirtualService CRD does not exist. Do not attempt to create it and log error
313+
err := fmt.Errorf("the VirtualService CRD is not present in this cluster")
314+
log.FromContext(ctx).Error(err, "InferenceService has service mesh annotation but VirtualService CRD not found")
315+
}
316+
298317
}
299318

300319
// Update the InferenceService
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
apiVersion: networking.istio.io/v1beta1
2+
kind: VirtualService
3+
metadata:
4+
name: {{ .VirtualServiceName }}
5+
namespace: {{ .Namespace }}
6+
spec:
7+
hosts:
8+
- {{ .Name }}.{{ .Namespace }}.svc.cluster.local
9+
http:
10+
- match:
11+
- port: 80
12+
route:
13+
- destination:
14+
host: {{ .Name }}.{{ .Namespace }}.svc.cluster.local
15+
port:
16+
number: 443

controllers/trustyaiservice_controller.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ type TrustyAIServiceReconciler struct {
7070
//+kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterrolebindings,verbs=get;list;watch;create;update;delete
7171
//+kubebuilder:rbac:groups=coordination.k8s.io,resources=leases,verbs=get;create;update
7272
//+kubebuilder:rbac:groups=networking.istio.io,resources=destinationrules,verbs=create;list;watch;get;update;patch;delete
73+
//+kubebuilder:rbac:groups=networking.istio.io,resources=virtualservices,verbs=create;list;watch;get;update;patch;delete
7374
//+kubebuilder:rbac:groups=apiextensions.k8s.io,resources=customresourcedefinitions,verbs=list;watch;get
7475

7576
// Reconcile is part of the main kubernetes reconciliation loop which aims to

controllers/utils.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,8 @@ func generateTLSServiceURL(crName string, namespace string) string {
7171
func generateNonTLSServiceURL(crName string, namespace string) string {
7272
return "http://" + crName + "." + namespace + ".svc"
7373
}
74+
75+
// generateKServeLoggerURL generates an logger url for KServe Inference Loggers
76+
func generateKServeLoggerURL(crName string, namespace string) string {
77+
return "http://" + crName + "." + namespace + ".svc.cluster.local"
78+
}

controllers/virtual_service.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package controllers
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"reflect"
7+
8+
trustyaiopendatahubiov1alpha1 "github.com/trustyai-explainability/trustyai-service-operator/api/v1alpha1"
9+
templateParser "github.com/trustyai-explainability/trustyai-service-operator/controllers/templates"
10+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
11+
"k8s.io/apimachinery/pkg/api/errors"
12+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
13+
"k8s.io/apimachinery/pkg/types"
14+
ctrl "sigs.k8s.io/controller-runtime"
15+
"sigs.k8s.io/controller-runtime/pkg/log"
16+
)
17+
18+
const (
19+
virtualServiceTemplatePath = "service/virtual-service.tmpl.yaml"
20+
virtualServiceCDRName = "destinationrules.networking.istio.io"
21+
)
22+
23+
// DestinationRuleConfig has the variables for the DestinationRule template
24+
type VirtualServiceConfig struct {
25+
Name string
26+
Namespace string
27+
VirtualServiceName string
28+
}
29+
30+
// isVirtualServiceCRDPresent returns true if the DestinationRule CRD is present, false otherwise
31+
func (r *TrustyAIServiceReconciler) isVirtualServiceCRDPresent(ctx context.Context) (bool, error) {
32+
crd := &apiextensionsv1.CustomResourceDefinition{}
33+
34+
err := r.Get(ctx, types.NamespacedName{Name: virtualServiceCDRName}, crd)
35+
if err != nil {
36+
if !errors.IsNotFound(err) {
37+
return false, fmt.Errorf("error getting "+virtualServiceCDRName+" CRD: %v", err)
38+
}
39+
// Not found
40+
return false, nil
41+
}
42+
43+
// Found
44+
return true, nil
45+
}
46+
47+
func (r *TrustyAIServiceReconciler) ensureVirtualService(ctx context.Context, instance *trustyaiopendatahubiov1alpha1.TrustyAIService) error {
48+
49+
virtualServiceName := instance.Name + "-redirect"
50+
51+
existingVirtualService := &unstructured.Unstructured{}
52+
existingVirtualService.SetKind("VirtualService")
53+
existingVirtualService.SetAPIVersion("networking.istio.io/v1beta1")
54+
55+
// Check if the DestinationRule already exists
56+
err := r.Get(ctx, types.NamespacedName{Name: virtualServiceName, Namespace: instance.Namespace}, existingVirtualService)
57+
if err == nil {
58+
// DestinationRule exists
59+
return nil
60+
}
61+
62+
if !errors.IsNotFound(err) {
63+
return fmt.Errorf("failed to check for existing VirtualService: %v", err)
64+
}
65+
66+
virtualServiceConfig := VirtualServiceConfig{
67+
Name: instance.Name,
68+
Namespace: instance.Namespace,
69+
VirtualServiceName: virtualServiceName,
70+
}
71+
72+
var virtualService *unstructured.Unstructured
73+
virtualService, err = templateParser.ParseResource[unstructured.Unstructured](virtualServiceTemplatePath, virtualServiceConfig, reflect.TypeOf(&unstructured.Unstructured{}))
74+
if err != nil {
75+
log.FromContext(ctx).Error(err, "could not parse the VirtualService template")
76+
return err
77+
}
78+
79+
if err := ctrl.SetControllerReference(instance, virtualService, r.Scheme); err != nil {
80+
return err
81+
}
82+
83+
err = r.Create(ctx, virtualService)
84+
if err != nil {
85+
return fmt.Errorf("failed to create VirtualService: %v", err)
86+
}
87+
88+
return nil
89+
}

0 commit comments

Comments
 (0)