diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 91be6698..be515044 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -154,6 +154,18 @@ rules: - patch - update - watch +- apiGroups: + - networking.istio.io + resources: + - virtualservices + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - rbac.authorization.k8s.io resources: diff --git a/controllers/inference_services.go b/controllers/inference_services.go index ee8745bf..827ed4f4 100644 --- a/controllers/inference_services.go +++ b/controllers/inference_services.go @@ -241,7 +241,7 @@ func (r *TrustyAIServiceReconciler) handleInferenceServices(ctx context.Context, // patchKServe adds a TrustyAI service as an InferenceLogger to a KServe InferenceService func (r *TrustyAIServiceReconciler) patchKServe(ctx context.Context, instance *trustyaiopendatahubiov1alpha1.TrustyAIService, infService kservev1beta1.InferenceService, namespace string, crName string, remove bool) error { - url := generateNonTLSServiceURL(crName, namespace) + url := generateKServeLoggerURL(crName, namespace) if remove { 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 log.FromContext(ctx).Error(err, "InferenceService has service mesh annotation but DestinationRule CRD not found") } + // Check if VirtualService CRD is present. If there's an error, don't proceed and return the error + exists, err = r.isVirtualServiceCRDPresent(ctx) + if err != nil { + log.FromContext(ctx).Error(err, "Error verifying VirtualService CRD is present") + return err + } + + // Try to create the VirtualService, since CRD exists + if exists { + err := r.ensureVirtualService(ctx, instance) + if err != nil { + return fmt.Errorf("failed to ensure VirtualService: %v", err) + } + } else { + // VirtualService CRD does not exist. Do not attempt to create it and log error + err := fmt.Errorf("the VirtualService CRD is not present in this cluster") + log.FromContext(ctx).Error(err, "InferenceService has service mesh annotation but VirtualService CRD not found") + } + } // Update the InferenceService diff --git a/controllers/templates/service/virtual-service.tmpl.yaml b/controllers/templates/service/virtual-service.tmpl.yaml new file mode 100644 index 00000000..8356be94 --- /dev/null +++ b/controllers/templates/service/virtual-service.tmpl.yaml @@ -0,0 +1,16 @@ +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: {{ .VirtualServiceName }} + namespace: {{ .Namespace }} +spec: + hosts: + - {{ .Name }}.{{ .Namespace }}.svc.cluster.local + http: + - match: + - port: 80 + route: + - destination: + host: {{ .Name }}.{{ .Namespace }}.svc.cluster.local + port: + number: 443 diff --git a/controllers/trustyaiservice_controller.go b/controllers/trustyaiservice_controller.go index 1176d986..a6ae3770 100644 --- a/controllers/trustyaiservice_controller.go +++ b/controllers/trustyaiservice_controller.go @@ -70,6 +70,7 @@ type TrustyAIServiceReconciler struct { //+kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterrolebindings,verbs=get;list;watch;create;update;delete //+kubebuilder:rbac:groups=coordination.k8s.io,resources=leases,verbs=get;create;update //+kubebuilder:rbac:groups=networking.istio.io,resources=destinationrules,verbs=create;list;watch;get;update;patch;delete +//+kubebuilder:rbac:groups=networking.istio.io,resources=virtualservices,verbs=create;list;watch;get;update;patch;delete //+kubebuilder:rbac:groups=apiextensions.k8s.io,resources=customresourcedefinitions,verbs=list;watch;get // Reconcile is part of the main kubernetes reconciliation loop which aims to diff --git a/controllers/utils.go b/controllers/utils.go index e96fc4e5..759b3880 100644 --- a/controllers/utils.go +++ b/controllers/utils.go @@ -71,3 +71,8 @@ func generateTLSServiceURL(crName string, namespace string) string { func generateNonTLSServiceURL(crName string, namespace string) string { return "http://" + crName + "." + namespace + ".svc" } + +// generateKServeLoggerURL generates an logger url for KServe Inference Loggers +func generateKServeLoggerURL(crName string, namespace string) string { + return "http://" + crName + "." + namespace + ".svc.cluster.local" +} diff --git a/controllers/virtual_service.go b/controllers/virtual_service.go new file mode 100644 index 00000000..f5223c86 --- /dev/null +++ b/controllers/virtual_service.go @@ -0,0 +1,89 @@ +package controllers + +import ( + "context" + "fmt" + "reflect" + + trustyaiopendatahubiov1alpha1 "github.com/trustyai-explainability/trustyai-service-operator/api/v1alpha1" + templateParser "github.com/trustyai-explainability/trustyai-service-operator/controllers/templates" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +const ( + virtualServiceTemplatePath = "service/virtual-service.tmpl.yaml" + virtualServiceCDRName = "destinationrules.networking.istio.io" +) + +// DestinationRuleConfig has the variables for the DestinationRule template +type VirtualServiceConfig struct { + Name string + Namespace string + VirtualServiceName string +} + +// isVirtualServiceCRDPresent returns true if the DestinationRule CRD is present, false otherwise +func (r *TrustyAIServiceReconciler) isVirtualServiceCRDPresent(ctx context.Context) (bool, error) { + crd := &apiextensionsv1.CustomResourceDefinition{} + + err := r.Get(ctx, types.NamespacedName{Name: virtualServiceCDRName}, crd) + if err != nil { + if !errors.IsNotFound(err) { + return false, fmt.Errorf("error getting "+virtualServiceCDRName+" CRD: %v", err) + } + // Not found + return false, nil + } + + // Found + return true, nil +} + +func (r *TrustyAIServiceReconciler) ensureVirtualService(ctx context.Context, instance *trustyaiopendatahubiov1alpha1.TrustyAIService) error { + + virtualServiceName := instance.Name + "-redirect" + + existingVirtualService := &unstructured.Unstructured{} + existingVirtualService.SetKind("VirtualService") + existingVirtualService.SetAPIVersion("networking.istio.io/v1beta1") + + // Check if the DestinationRule already exists + err := r.Get(ctx, types.NamespacedName{Name: virtualServiceName, Namespace: instance.Namespace}, existingVirtualService) + if err == nil { + // DestinationRule exists + return nil + } + + if !errors.IsNotFound(err) { + return fmt.Errorf("failed to check for existing VirtualService: %v", err) + } + + virtualServiceConfig := VirtualServiceConfig{ + Name: instance.Name, + Namespace: instance.Namespace, + VirtualServiceName: virtualServiceName, + } + + var virtualService *unstructured.Unstructured + virtualService, err = templateParser.ParseResource[unstructured.Unstructured](virtualServiceTemplatePath, virtualServiceConfig, reflect.TypeOf(&unstructured.Unstructured{})) + if err != nil { + log.FromContext(ctx).Error(err, "could not parse the VirtualService template") + return err + } + + if err := ctrl.SetControllerReference(instance, virtualService, r.Scheme); err != nil { + return err + } + + err = r.Create(ctx, virtualService) + if err != nil { + return fmt.Errorf("failed to create VirtualService: %v", err) + } + + return nil +}