diff --git a/controllers/comparators/clusterrolebinding_comparator.go b/controllers/comparators/clusterrolebinding_comparator.go new file mode 100644 index 00000000..9468123c --- /dev/null +++ b/controllers/comparators/clusterrolebinding_comparator.go @@ -0,0 +1,31 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package comparators + +import ( + v1 "k8s.io/api/rbac/v1" + "reflect" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func GetClusterRoleBindingComparator() ResourceComparator { + return func(deployed client.Object, requested client.Object) bool { + deployedCRB := deployed.(*v1.ClusterRoleBinding) + requestedCRB := requested.(*v1.ClusterRoleBinding) + return reflect.DeepEqual(deployedCRB.RoleRef, requestedCRB.RoleRef) && + reflect.DeepEqual(deployedCRB.Subjects, requestedCRB.Subjects) + } +} diff --git a/controllers/comparators/networkpolicy_comparator.go b/controllers/comparators/networkpolicy_comparator.go new file mode 100644 index 00000000..3b0717ee --- /dev/null +++ b/controllers/comparators/networkpolicy_comparator.go @@ -0,0 +1,31 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package comparators + +import ( + "k8s.io/api/networking/v1" + "reflect" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func GetNetworkPolicyComparator() ResourceComparator { + return func(deployed client.Object, requested client.Object) bool { + deployedCRB := deployed.(*v1.NetworkPolicy) + requestedCRB := requested.(*v1.NetworkPolicy) + return reflect.DeepEqual(deployedCRB.Spec, requestedCRB.Spec) && + reflect.DeepEqual(deployedCRB.Labels, requestedCRB.Labels) + } +} diff --git a/controllers/comparators/peerAuthentication_comparator.go b/controllers/comparators/peerAuthentication_comparator.go new file mode 100644 index 00000000..251f766f --- /dev/null +++ b/controllers/comparators/peerAuthentication_comparator.go @@ -0,0 +1,32 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package comparators + +import ( + istiosecv1beta1 "istio.io/client-go/pkg/apis/security/v1beta1" + "reflect" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func GetPeerAuthenticationComparator() ResourceComparator { + return func(deployed client.Object, requested client.Object) bool { + deployedPeerAuthentication := deployed.(*istiosecv1beta1.PeerAuthentication) + requestedPeerAuthentication := requested.(*istiosecv1beta1.PeerAuthentication) + return reflect.DeepEqual(deployedPeerAuthentication.Spec.Selector, requestedPeerAuthentication.Spec.Selector) && + reflect.DeepEqual(deployedPeerAuthentication.Spec.Mtls, requestedPeerAuthentication.Spec.Mtls) && + reflect.DeepEqual(deployedPeerAuthentication.Spec.PortLevelMtls, requestedPeerAuthentication.Spec.PortLevelMtls) + } +} diff --git a/controllers/comparators/podmonitor_comparator.go b/controllers/comparators/podmonitor_comparator.go new file mode 100644 index 00000000..e36f60ae --- /dev/null +++ b/controllers/comparators/podmonitor_comparator.go @@ -0,0 +1,30 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package comparators + +import ( + v1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" + "reflect" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func GetPodMonitorComparator() ResourceComparator { + return func(deployed client.Object, requested client.Object) bool { + deployedPodMonitor := deployed.(*v1.PodMonitor) + requestedPodMonitor := requested.(*v1.PodMonitor) + return reflect.DeepEqual(deployedPodMonitor.Spec, requestedPodMonitor.Spec) + } +} diff --git a/controllers/comparators/resourcecomparator.go b/controllers/comparators/resourcecomparator.go index 6fbe49e9..ecda1fb6 100644 --- a/controllers/comparators/resourcecomparator.go +++ b/controllers/comparators/resourcecomparator.go @@ -1,3 +1,18 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package comparators import ( @@ -5,6 +20,4 @@ import ( ) // ResourceComparator would compare deployed & requested resource. It would retrun `true` if both resource are same else it would return `false` -type ResourceComparator interface { - Compare(deployed client.Object, requested client.Object) bool -} +type ResourceComparator func(deployed client.Object, requested client.Object) bool diff --git a/controllers/comparators/rolebinding_comparator.go b/controllers/comparators/rolebinding_comparator.go new file mode 100644 index 00000000..e1bca166 --- /dev/null +++ b/controllers/comparators/rolebinding_comparator.go @@ -0,0 +1,31 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package comparators + +import ( + v1 "k8s.io/api/rbac/v1" + "reflect" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func GetRoleBindingComparator() ResourceComparator { + return func(deployed client.Object, requested client.Object) bool { + deployedRB := deployed.(*v1.RoleBinding) + requestedRB := requested.(*v1.RoleBinding) + return reflect.DeepEqual(deployedRB.RoleRef, requestedRB.RoleRef) && + reflect.DeepEqual(deployedRB.Subjects, requestedRB.Subjects) + } +} diff --git a/controllers/comparators/route_comparator.go b/controllers/comparators/route_comparator.go index c65e4872..9c1fa4b7 100644 --- a/controllers/comparators/route_comparator.go +++ b/controllers/comparators/route_comparator.go @@ -1,18 +1,48 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package comparators import ( v1 "github.com/openshift/api/route/v1" - "k8s.io/apimachinery/pkg/api/equality" + "reflect" "sigs.k8s.io/controller-runtime/pkg/client" ) -type RouteComparator struct { +func GetMMRouteComparator() ResourceComparator { + return func(deployed client.Object, requested client.Object) bool { + deployedRoute := deployed.(*v1.Route) + requestedRoute := requested.(*v1.Route) + return reflect.DeepEqual(deployedRoute.Spec.To, requestedRoute.Spec.To) && + reflect.DeepEqual(deployedRoute.Spec.Port, requestedRoute.Spec.Port) && + reflect.DeepEqual(deployedRoute.Spec.WildcardPolicy, requestedRoute.Spec.WildcardPolicy) && + reflect.DeepEqual(deployedRoute.Spec.Path, requestedRoute.Spec.Path) && + reflect.DeepEqual(deployedRoute.Spec.TLS, requestedRoute.Spec.TLS) && + reflect.DeepEqual(deployedRoute.Labels, requestedRoute.Labels) + } } -func (c *RouteComparator) Compare(deployed client.Object, requested client.Object) bool { - deployedRoute := deployed.(*v1.Route) - requestedRoute := requested.(*v1.Route) - return equality.Semantic.DeepEqual(deployedRoute.Spec, requestedRoute.Spec) && - equality.Semantic.DeepEqual(deployedRoute.Annotations, requestedRoute.Annotations) && - equality.Semantic.DeepEqual(deployedRoute.Labels, requestedRoute.Labels) +func GetKServeRouteComparator() ResourceComparator { + return func(deployed client.Object, requested client.Object) bool { + deployedRoute := deployed.(*v1.Route) + requestedRoute := requested.(*v1.Route) + return reflect.DeepEqual(deployedRoute.Spec.Host, requestedRoute.Spec.Host) && + reflect.DeepEqual(deployedRoute.Spec.To, requestedRoute.Spec.To) && + reflect.DeepEqual(deployedRoute.Spec.Port, requestedRoute.Spec.Port) && + reflect.DeepEqual(deployedRoute.Spec.TLS, requestedRoute.Spec.TLS) && + reflect.DeepEqual(deployedRoute.Spec.WildcardPolicy, requestedRoute.Spec.WildcardPolicy) && + reflect.DeepEqual(deployedRoute.ObjectMeta.Labels, requestedRoute.ObjectMeta.Labels) + } } diff --git a/controllers/comparators/service_comparator.go b/controllers/comparators/service_comparator.go new file mode 100644 index 00000000..f4032d1a --- /dev/null +++ b/controllers/comparators/service_comparator.go @@ -0,0 +1,34 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package comparators + +import ( + v1 "k8s.io/api/core/v1" + "reflect" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func GetServiceComparator() ResourceComparator { + return func(deployed client.Object, requested client.Object) bool { + deployedService := deployed.(*v1.Service) + requestedService := requested.(*v1.Service) + return reflect.DeepEqual(deployedService.Spec.Ports, requestedService.Spec.Ports) && + reflect.DeepEqual(deployedService.Spec.Type, requestedService.Spec.Type) && + reflect.DeepEqual(deployedService.Spec.Selector, requestedService.Spec.Selector) && + reflect.DeepEqual(deployedService.Annotations, requestedService.Annotations) && + reflect.DeepEqual(deployedService.Labels, requestedService.Labels) + } +} diff --git a/controllers/comparators/serviceaccount_comparator.go b/controllers/comparators/serviceaccount_comparator.go new file mode 100644 index 00000000..f7a0c398 --- /dev/null +++ b/controllers/comparators/serviceaccount_comparator.go @@ -0,0 +1,26 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package comparators + +import ( + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func GetServiceAccountComparator() ResourceComparator { + return func(deployed client.Object, requested client.Object) bool { + return true + } +} diff --git a/controllers/comparators/servicemonitor_comparator.go b/controllers/comparators/servicemonitor_comparator.go new file mode 100644 index 00000000..2ee07a60 --- /dev/null +++ b/controllers/comparators/servicemonitor_comparator.go @@ -0,0 +1,30 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package comparators + +import ( + v1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" + "reflect" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func GetServiceMonitorComparator() ResourceComparator { + return func(deployed client.Object, requested client.Object) bool { + deployedServiceMonitor := deployed.(*v1.ServiceMonitor) + requestedServiceMonitor := requested.(*v1.ServiceMonitor) + return reflect.DeepEqual(deployedServiceMonitor.Spec, requestedServiceMonitor.Spec) + } +} diff --git a/controllers/comparators/smmr_comparator.go b/controllers/comparators/smmr_comparator.go new file mode 100644 index 00000000..7ad81c66 --- /dev/null +++ b/controllers/comparators/smmr_comparator.go @@ -0,0 +1,30 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package comparators + +import ( + v1 "maistra.io/api/core/v1" + "reflect" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func GetServiceMeshMemberRollComparator() ResourceComparator { + return func(deployed client.Object, requested client.Object) bool { + deployedSMMR := deployed.(*v1.ServiceMeshMemberRoll) + requestedSMMR := requested.(*v1.ServiceMeshMemberRoll) + return reflect.DeepEqual(deployedSMMR.Spec, requestedSMMR.Spec) + } +} diff --git a/controllers/comparators/telemetry_comparator.go b/controllers/comparators/telemetry_comparator.go new file mode 100644 index 00000000..108073e6 --- /dev/null +++ b/controllers/comparators/telemetry_comparator.go @@ -0,0 +1,31 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package comparators + +import ( + telemetryv1alpha1 "istio.io/client-go/pkg/apis/telemetry/v1alpha1" + "reflect" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func GetTelemetryComparator() ResourceComparator { + return func(deployed client.Object, requested client.Object) bool { + deployedTelemetry := deployed.(*telemetryv1alpha1.Telemetry) + requestedTelemetry := requested.(*telemetryv1alpha1.Telemetry) + return reflect.DeepEqual(deployedTelemetry.Spec.Selector, requestedTelemetry.Spec.Selector) && + reflect.DeepEqual(deployedTelemetry.Spec.Metrics, requestedTelemetry.Spec.Metrics) + } +} diff --git a/controllers/components/route.go b/controllers/components/route.go deleted file mode 100644 index 87216426..00000000 --- a/controllers/components/route.go +++ /dev/null @@ -1,66 +0,0 @@ -package components - -import ( - "context" - "fmt" - "github.com/go-logr/logr" - "github.com/opendatahub-io/odh-model-controller/controllers/comparators" - v1 "github.com/openshift/api/route/v1" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -// RouteHandler to provide route specific implementation. -type RouteHandler interface { - FetchRoute(key types.NamespacedName) (*v1.Route, error) - GetComparator() comparators.ResourceComparator - DeleteRoute(key types.NamespacedName) error -} - -type routeHandler struct { - client.Client - ctx context.Context - log logr.Logger -} - -func NewRouteHandler(client client.Client, ctx context.Context, log logr.Logger) RouteHandler { - return &routeHandler{ - Client: client, - ctx: ctx, - log: log, - } -} - -func (r *routeHandler) FetchRoute(key types.NamespacedName) (*v1.Route, error) { - route := &v1.Route{} - err := r.Get(r.ctx, key, route) - if err != nil && errors.IsNotFound(err) { - r.log.Info("Openshift Route not found.") - return nil, nil - } else if err != nil { - return nil, err - } - r.log.Info("Successfully fetch deployed Openshift Route") - return route, nil -} - -func (r *routeHandler) GetComparator() comparators.ResourceComparator { - return &comparators.RouteComparator{} -} - -func (r *routeHandler) DeleteRoute(key types.NamespacedName) error { - route := &v1.Route{} - err := r.Get(r.ctx, key, route) - if err != nil { - if errors.IsNotFound(err) { - return nil - } - return err - } - if err = r.Delete(r.ctx, route); err != nil { - return fmt.Errorf("failed to delete route: %w", err) - } - - return nil -} diff --git a/controllers/constants/constants.go b/controllers/constants/constants.go index cd654cfd..87ea27af 100644 --- a/controllers/constants/constants.go +++ b/controllers/constants/constants.go @@ -1,3 +1,18 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package constants const ( diff --git a/controllers/inferecenceservice_serviceaccount.go b/controllers/inferecenceservice_serviceaccount.go deleted file mode 100644 index a7f41d6c..00000000 --- a/controllers/inferecenceservice_serviceaccount.go +++ /dev/null @@ -1,171 +0,0 @@ -/* - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package controllers - -import ( - "context" - "reflect" - - authv1 "k8s.io/api/rbac/v1" - apierrs "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/types" - - kservev1beta1 "github.com/kserve/kserve/pkg/apis/serving/v1beta1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/util/retry" - ctrl "sigs.k8s.io/controller-runtime" -) - -const ( - modelMeshServiceAccountName = "modelmesh-serving-sa" -) - -func newInferenceServiceSA(inferenceservice *kservev1beta1.InferenceService) *corev1.ServiceAccount { - return &corev1.ServiceAccount{ - ObjectMeta: metav1.ObjectMeta{ - Name: modelMeshServiceAccountName, - Namespace: inferenceservice.Namespace, - }, - } -} - -func createDelegateClusterRoleBinding(serviceAccountName string, serviceAccountNamespace string) *authv1.ClusterRoleBinding { - return &authv1.ClusterRoleBinding{ - ObjectMeta: metav1.ObjectMeta{ - Name: serviceAccountNamespace + "-" + serviceAccountName + "-auth-delegator", - Namespace: serviceAccountNamespace, - }, - Subjects: []authv1.Subject{ - authv1.Subject{ - Kind: "ServiceAccount", - Namespace: serviceAccountNamespace, - Name: serviceAccountName, - }, - }, - RoleRef: authv1.RoleRef{ - Kind: "ClusterRole", - Name: "system:auth-delegator", - }, - } -} - -func (r *OpenshiftInferenceServiceReconciler) reconcileSA(inferenceService *kservev1beta1.InferenceService, ctx context.Context, newSA func(service *kservev1beta1.InferenceService) *corev1.ServiceAccount) error { - - // Initialize logger format - log := r.Log.WithValues("inferenceservice", inferenceService.Name, "namespace", inferenceService.Namespace) - - desiredSA := newSA(inferenceService) - foundSA := &corev1.ServiceAccount{} - - err := r.Get(ctx, types.NamespacedName{ - Name: desiredSA.Name, - Namespace: inferenceService.Namespace, - }, foundSA) - - if err != nil { - if apierrs.IsNotFound(err) { - log.Info("Creating Auth Delegation Service Account") - // Add .metatada.ownerReferences to the service account to be deleted by the - // Kubernetes garbage collector if the predictor is deleted - err = ctrl.SetControllerReference(inferenceService, desiredSA, r.Scheme) - if err != nil { - log.Error(err, "Unable to add OwnerReference to the Auth Delegation Service Account") - return err - } - // Create the SA in the Openshift cluster - err = r.Create(ctx, desiredSA) - if err != nil && !apierrs.IsAlreadyExists(err) { - log.Error(err, "Unable to create the Auth Delegation Service Account") - return err - } - } else { - log.Error(err, "Unable to fetch the Auth Delegation Service Account") - return err - } - } - - // Create the corresponding auth delegation cluster role binding - desiredCRB := createDelegateClusterRoleBinding(modelMeshServiceAccountName, desiredSA.Namespace) - foundCRB := &authv1.ClusterRoleBinding{} - justCreated := false - - err = r.Get(ctx, types.NamespacedName{ - Name: desiredCRB.Name, - Namespace: desiredCRB.Namespace, - }, foundCRB) - - if err != nil { - if apierrs.IsNotFound(err) { - log.Info("Creating Auth Delegation Cluster Role Binding") - // Add .metatada.ownerReferences to the CRB to be deleted by the - // Kubernetes garbage collector if the predictor is deleted - err = ctrl.SetControllerReference(inferenceService, desiredCRB, r.Scheme) - if err != nil { - log.Error(err, "Unable to add OwnerReference to the Auth Delegation Cluster Role Binding") - return err - } - // Create the CRB in the Openshift cluster - err = r.Create(ctx, desiredCRB) - if err != nil && !apierrs.IsAlreadyExists(err) { - log.Error(err, "Unable to create the Auth Delegation Cluster Role Binding") - return err - } - justCreated = true - } else { - log.Error(err, "Unable to fetch the Auth Delegation Cluster Role Binding") - return err - } - } - - // Reconcile the CRB spec if it has been manually modified - if !justCreated && !CompareInferenceServiceCRBs(*desiredCRB, *foundCRB) { - log.Info("Reconciling Auth Delegation Cluster Role Binding") - // Retry the update operation when the ingress controller eventually - // updates the resource version field - err := retry.RetryOnConflict(retry.DefaultRetry, func() error { - // Get the last CRB revision - if err := r.Get(ctx, types.NamespacedName{ - Name: desiredCRB.Name, - Namespace: desiredCRB.Namespace, - }, foundCRB); err != nil { - return err - } - // Reconcile labels and spec field - foundCRB.Subjects = desiredCRB.Subjects - foundCRB.RoleRef = desiredCRB.RoleRef - return r.Update(ctx, desiredCRB) - }) - if err != nil { - log.Error(err, "Unable to reconcile the Auth Delegation Cluster Role Binding") - return err - } - } - return nil -} - -// ReconcileSA will manage the creation, update and deletion of the auth delegation SA + RBAC -func (r *OpenshiftInferenceServiceReconciler) ReconcileSA( - inferenceservice *kservev1beta1.InferenceService, ctx context.Context) error { - return r.reconcileSA(inferenceservice, ctx, newInferenceServiceSA) -} - -// CompareInferenceServiceCRBs checks if two service accounts are equal, if not return false -func CompareInferenceServiceCRBs(crb1 authv1.ClusterRoleBinding, crb2 authv1.ClusterRoleBinding) bool { - // Two CRBs will be equal if the role reference and subjects are equal - return reflect.DeepEqual(crb1.RoleRef, crb2.RoleRef) && - reflect.DeepEqual(crb1.Subjects, crb2.Subjects) -} diff --git a/controllers/inferenceservice_controller.go b/controllers/inferenceservice_controller.go index 297d9292..d89e9cc0 100644 --- a/controllers/inferenceservice_controller.go +++ b/controllers/inferenceservice_controller.go @@ -17,10 +17,11 @@ package controllers import ( "context" - "github.com/go-logr/logr" kservev1alpha1 "github.com/kserve/kserve/pkg/apis/serving/v1alpha1" kservev1beta1 "github.com/kserve/kserve/pkg/apis/serving/v1beta1" + "github.com/opendatahub-io/odh-model-controller/controllers/reconcilers" + "github.com/opendatahub-io/odh-model-controller/controllers/utils" routev1 "github.com/openshift/api/route/v1" monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" corev1 "k8s.io/api/core/v1" @@ -38,40 +39,39 @@ import ( // OpenshiftInferenceServiceReconciler holds the controller configuration. type OpenshiftInferenceServiceReconciler struct { - client.Client - Scheme *runtime.Scheme - Log logr.Logger - MeshDisabled bool + client client.Client + scheme *runtime.Scheme + log logr.Logger + MeshDisabled bool + mmISVCReconciler *reconcilers.ModelMeshInferenceServiceReconciler + kserveISVCReconciler *reconcilers.KserveInferenceServiceReconciler } -const ( - inferenceServiceDeploymentModeAnnotation = "serving.kserve.io/deploymentMode" - inferenceServiceDeploymentModeAnnotationValue = "ModelMesh" -) - -func (r *OpenshiftInferenceServiceReconciler) isDeploymentModeForIsvcModelMesh(inferenceservice *kservev1beta1.InferenceService) bool { - value, exists := inferenceservice.Annotations[inferenceServiceDeploymentModeAnnotation] - if exists && value == inferenceServiceDeploymentModeAnnotationValue { - return true +func NewOpenshiftInferenceServiceReconciler(client client.Client, scheme *runtime.Scheme, log logr.Logger, meshDisabled bool) *OpenshiftInferenceServiceReconciler { + return &OpenshiftInferenceServiceReconciler{ + client: client, + scheme: scheme, + log: log, + MeshDisabled: meshDisabled, + mmISVCReconciler: reconcilers.NewModelMeshInferenceServiceReconciler(client, scheme), + kserveISVCReconciler: reconcilers.NewKServeInferenceServiceReconciler(client, scheme), } - return false } // Reconcile performs the reconciling of the Openshift objects for a Kubeflow // InferenceService. func (r *OpenshiftInferenceServiceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { // Initialize logger format - log := r.Log.WithValues("InferenceService", req.Name, "namespace", req.Namespace) - + log := r.log.WithValues("InferenceService", req.Name, "namespace", req.Namespace) // Get the InferenceService object when a reconciliation event is triggered (create, // update, delete) - inferenceservice := &kservev1beta1.InferenceService{} - err := r.Get(ctx, req.NamespacedName, inferenceservice) + isvc := &kservev1beta1.InferenceService{} + err := r.client.Get(ctx, req.NamespacedName, isvc) if err != nil && apierrs.IsNotFound(err) { log.Info("Stop InferenceService reconciliation") - // InferenceService not found, so we check for any other inference services that might be using Kserve - // If none are found, we delete the common namespace-scoped resources that were created for Kserve Metrics. - err1 := r.DeleteKserveMetricsResourcesIfNoKserveIsvcExists(ctx, req, req.Namespace) + // InferenceService not found, so we check for any other inference services that might be using Kserve/ModelMesh + // If none are found, we delete the common namespace-scoped resources that were created for Kserve/ModelMesh. + err1 := r.DeleteResourcesIfNoIsvcExists(ctx, log, req.Namespace) if err1 != nil { log.Error(err1, "Unable to clean up resources") return ctrl.Result{}, err1 @@ -82,20 +82,17 @@ func (r *OpenshiftInferenceServiceReconciler) Reconcile(ctx context.Context, req return ctrl.Result{}, err } - if inferenceservice.GetDeletionTimestamp() != nil { - return reconcile.Result{}, r.onDeletion(ctx, inferenceservice) + if isvc.GetDeletionTimestamp() != nil { + return reconcile.Result{}, r.onDeletion(ctx, log, isvc) } // Check what deployment mode is used by the InferenceService. We have differing reconciliation logic for Kserve and ModelMesh - if r.isDeploymentModeForIsvcModelMesh(inferenceservice) { + if utils.IsDeploymentModeForIsvcModelMesh(isvc) { log.Info("Reconciling InferenceService for ModelMesh") - err = r.ReconcileModelMeshInference(ctx, req, inferenceservice) - if err != nil { - return ctrl.Result{}, err - } + err = r.mmISVCReconciler.Reconcile(ctx, log, isvc) } else { log.Info("Reconciling InferenceService for Kserve") - err = r.ReconcileKserveInference(ctx, req, inferenceservice) + err = r.kserveISVCReconciler.Reconcile(ctx, log, isvc) } return ctrl.Result{}, err @@ -117,19 +114,19 @@ func (r *OpenshiftInferenceServiceReconciler) SetupWithManager(mgr ctrl.Manager) Owns(&monitoringv1.PodMonitor{}). Watches(&source.Kind{Type: &kservev1alpha1.ServingRuntime{}}, handler.EnqueueRequestsFromMapFunc(func(o client.Object) []reconcile.Request { - r.Log.Info("Reconcile event triggered by serving runtime: " + o.GetName()) + r.log.Info("Reconcile event triggered by serving runtime: " + o.GetName()) inferenceServicesList := &kservev1beta1.InferenceServiceList{} opts := []client.ListOption{client.InNamespace(o.GetNamespace())} // Todo: Get only Inference Services that are deploying on the specific serving runtime - err := r.List(context.TODO(), inferenceServicesList, opts...) + err := r.client.List(context.TODO(), inferenceServicesList, opts...) if err != nil { - r.Log.Info("Error getting list of inference services for namespace") + r.log.Info("Error getting list of inference services for namespace") return []reconcile.Request{} } if len(inferenceServicesList.Items) == 0 { - r.Log.Info("No InferenceServices found for Serving Runtime: " + o.GetName()) + r.log.Info("No InferenceServices found for Serving Runtime: " + o.GetName()) return []reconcile.Request{} } @@ -153,13 +150,22 @@ func (r *OpenshiftInferenceServiceReconciler) SetupWithManager(mgr ctrl.Manager) } // general clean-up, mostly resources in different namespaces from kservev1beta1.InferenceService -func (r *OpenshiftInferenceServiceReconciler) onDeletion(ctx context.Context, inferenceService *kservev1beta1.InferenceService) error { - log := r.Log.WithValues("InferenceService", inferenceService.Name, "namespace", inferenceService.Namespace) +func (r *OpenshiftInferenceServiceReconciler) onDeletion(ctx context.Context, log logr.Logger, inferenceService *kservev1beta1.InferenceService) error { log.V(1).Info("Running cleanup logic") - if !r.isDeploymentModeForIsvcModelMesh(inferenceService) { + if !utils.IsDeploymentModeForIsvcModelMesh(inferenceService) { log.V(1).Info("Deleting kserve inference resource") - return r.OnDeletionOfKserveInferenceService(ctx, inferenceService) + return r.kserveISVCReconciler.OnDeletionOfKserveInferenceService(ctx, log, inferenceService) + } + return nil +} + +func (r *OpenshiftInferenceServiceReconciler) DeleteResourcesIfNoIsvcExists(ctx context.Context, log logr.Logger, isvcNamespace string) error { + if err := r.kserveISVCReconciler.DeleteKserveMetricsResourcesIfNoKserveIsvcExists(ctx, log, isvcNamespace); err != nil { + return err + } + if err := r.mmISVCReconciler.DeleteModelMeshResourcesIfNoMMIsvcExists(ctx, log, isvcNamespace); err != nil { + return err } return nil } diff --git a/controllers/inferenceservice_meshmember.go b/controllers/inferenceservice_meshmember.go deleted file mode 100644 index d649d1fe..00000000 --- a/controllers/inferenceservice_meshmember.go +++ /dev/null @@ -1,124 +0,0 @@ -/* - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package controllers - -import ( - "context" - "reflect" - - kservev1beta1 "github.com/kserve/kserve/pkg/apis/serving/v1beta1" - apierrs "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/util/retry" - maistrav1 "maistra.io/api/core/v1" - ctrl "sigs.k8s.io/controller-runtime" -) - -// NewInferenceServiceMeshMember defines the desired MeshMember object -func NewInferenceServiceMeshMember(inferenceservice *kservev1beta1.InferenceService) *maistrav1.ServiceMeshMember { - return &maistrav1.ServiceMeshMember{ - TypeMeta: metav1.TypeMeta{}, - // The name MUST be default, per the maistra docs - ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: inferenceservice.Namespace, Labels: map[string]string{"inferenceservice-name": inferenceservice.Name}}, - Spec: maistrav1.ServiceMeshMemberSpec{ - ControlPlaneRef: maistrav1.ServiceMeshControlPlaneRef{ - Name: "odh", - Namespace: "istio-system", - }, - }, - } -} - -// CompareInferenceServiceMeshMembers checks if two MeshMembers are equal, if not return false -func CompareInferenceServiceMeshMembers(mm1 *maistrav1.ServiceMeshMember, mm2 *maistrav1.ServiceMeshMember) bool { - // Two MeshMembers will be equal if the labels and spec are identical - return reflect.DeepEqual(mm1.ObjectMeta.Labels, mm2.ObjectMeta.Labels) -} - -// Reconcile will manage the creation, update and deletion of the MeshMember returned -// by the newMeshMember function -func (r *OpenshiftInferenceServiceReconciler) reconcileMeshMember(inferenceservice *kservev1beta1.InferenceService, - ctx context.Context, newMeshMember func(*kservev1beta1.InferenceService) *maistrav1.ServiceMeshMember) error { - // Initialize logger format - log := r.Log.WithValues("InferenceService", inferenceservice.Name, "namespace", inferenceservice.Namespace) - - // Generate the desired ServiceMeshMember - desiredMeshMember := newMeshMember(inferenceservice) - - // Create the ServiceMeshMember if it does not already exist - foundMeshMember := &maistrav1.ServiceMeshMember{} - justCreated := false - err := r.Get(ctx, types.NamespacedName{ - Name: desiredMeshMember.Name, - Namespace: inferenceservice.Namespace, - }, foundMeshMember) - if err != nil { - if apierrs.IsNotFound(err) { - log.Info("Creating ServiceMeshMember") - // Add .metatada.ownerReferences to the MeshMember to be deleted by the - // Kubernetes garbage collector if the Predictor is deleted - err = ctrl.SetControllerReference(inferenceservice, desiredMeshMember, r.Scheme) - if err != nil { - log.Error(err, "Unable to add OwnerReference to the MeshMember") - return err - } - // Create the ServiceMeshMember in the Openshift cluster - err = r.Create(ctx, desiredMeshMember) - if err != nil && !apierrs.IsAlreadyExists(err) { - log.Error(err, "Unable to create the ServiceMeshMember") - return err - } - justCreated = true - } else { - log.Error(err, "Unable to fetch the ServiceMeshMember") - return err - } - } - - // Reconcile the MeshMember spec if it has been manually modified - if !justCreated && !CompareInferenceServiceMeshMembers(desiredMeshMember, foundMeshMember) { - log.Info("Reconciling ServiceMeshMember") - // Retry the update operation when the ingress controller eventually - // updates the resource version field - err := retry.RetryOnConflict(retry.DefaultRetry, func() error { - // Get the last MeshMember revision - if err := r.Get(ctx, types.NamespacedName{ - Name: desiredMeshMember.Name, - Namespace: inferenceservice.Namespace, - }, foundMeshMember); err != nil { - return err - } - // Reconcile labels and spec field - foundMeshMember.Spec = *desiredMeshMember.Spec.DeepCopy() - foundMeshMember.ObjectMeta.Labels = desiredMeshMember.ObjectMeta.Labels - return r.Update(ctx, foundMeshMember) - }) - if err != nil { - log.Error(err, "Unable to reconcile the ServiceMeshMember") - return err - } - } - - return nil -} - -// ReconcileMeshMember will manage the creation, update and deletion of the -// MeshMember when the Predictor is reconciled -func (r *OpenshiftInferenceServiceReconciler) ReconcileMeshMember( - inferenceservice *kservev1beta1.InferenceService, ctx context.Context) error { - return r.reconcileMeshMember(inferenceservice, ctx, NewInferenceServiceMeshMember) -} diff --git a/controllers/inferenceservice_route.go b/controllers/inferenceservice_route.go deleted file mode 100644 index c93895f2..00000000 --- a/controllers/inferenceservice_route.go +++ /dev/null @@ -1,248 +0,0 @@ -/* - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package controllers - -import ( - "context" - "reflect" - "sort" - - "github.com/go-logr/logr" - kservev1alpha1 "github.com/kserve/kserve/pkg/apis/serving/v1alpha1" - kservev1beta1 "github.com/kserve/kserve/pkg/apis/serving/v1beta1" - routev1 "github.com/openshift/api/route/v1" - apierrs "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/intstr" - "k8s.io/client-go/util/retry" - "k8s.io/utils/pointer" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -const ( - modelmeshServiceName = "modelmesh-serving" - modelmeshAuthServicePort = 8443 - modelmeshServicePort = 8008 -) - -// NewInferenceServiceRoute defines the desired route object -func NewInferenceServiceRoute(inferenceservice *kservev1beta1.InferenceService, enableAuth bool) *routev1.Route { - - finalRoute := &routev1.Route{ - ObjectMeta: metav1.ObjectMeta{ - Name: inferenceservice.Name, - Namespace: inferenceservice.Namespace, - Labels: map[string]string{ - "inferenceservice-name": inferenceservice.Name, - }, - }, - Spec: routev1.RouteSpec{ - To: routev1.RouteTargetReference{ - Kind: "Service", - Name: modelmeshServiceName, - Weight: pointer.Int32Ptr(100), - }, - Port: &routev1.RoutePort{ - TargetPort: intstr.FromInt(modelmeshServicePort), - }, - WildcardPolicy: routev1.WildcardPolicyNone, - Path: "/v2/models/" + inferenceservice.Name, - }, - Status: routev1.RouteStatus{ - Ingress: []routev1.RouteIngress{}, - }, - } - - if enableAuth { - finalRoute.Spec.Port = &routev1.RoutePort{ - TargetPort: intstr.FromInt(modelmeshAuthServicePort), - } - finalRoute.Spec.TLS = &routev1.TLSConfig{ - Termination: routev1.TLSTerminationReencrypt, - InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyRedirect, - } - } else { - finalRoute.Spec.TLS = &routev1.TLSConfig{ - Termination: routev1.TLSTerminationEdge, - InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyRedirect, - } - } - - return finalRoute -} - -// CompareInferenceServiceRoutes checks if two routes are equal, if not return false -func CompareInferenceServiceRoutes(r1 routev1.Route, r2 routev1.Route) bool { - // Omit the host field since it is reconciled by the ingress controller - r1.Spec.Host, r2.Spec.Host = "", "" - - // Two routes will be equal if the labels and spec are identical - return reflect.DeepEqual(r1.ObjectMeta.Labels, r2.ObjectMeta.Labels) && - reflect.DeepEqual(r1.Spec, r2.Spec) -} - -func (r *OpenshiftInferenceServiceReconciler) findSupportingRuntimeForISvc(ctx context.Context, log logr.Logger, inferenceservice *kservev1beta1.InferenceService) *kservev1alpha1.ServingRuntime { - desiredServingRuntime := kservev1alpha1.ServingRuntime{} - - if inferenceservice.Spec.Predictor.Model.Runtime != nil { - err := r.Get(ctx, types.NamespacedName{ - Name: *inferenceservice.Spec.Predictor.Model.Runtime, - Namespace: inferenceservice.Namespace, - }, &desiredServingRuntime) - if err != nil { - if apierrs.IsNotFound(err) { - log.Info("Runtime specified in InferenceService does not exist", "runtime", *inferenceservice.Spec.Predictor.Model.Runtime) - } - } - return &desiredServingRuntime - } else { - runtimes := &kservev1alpha1.ServingRuntimeList{} - err := r.List(ctx, runtimes, client.InNamespace(inferenceservice.Namespace)) - if err != nil { - log.Error(err, "Listing ServingRuntimes failed") - return nil - } - - // Sort by creation date, to be somewhat deterministic - sort.Slice(runtimes.Items, func(i, j int) bool { - // Sorting descending by creation time leads to picking the most recently created runtimes first - if runtimes.Items[i].CreationTimestamp.Before(&runtimes.Items[j].CreationTimestamp) { - return false - } - if runtimes.Items[i].CreationTimestamp.Equal(&runtimes.Items[j].CreationTimestamp) { - // For Runtimes created at the same time, use alphabetical order. - return runtimes.Items[i].Name < runtimes.Items[j].Name - } - return true - }) - - for _, runtime := range runtimes.Items { - if runtime.Spec.Disabled != nil && *runtime.Spec.Disabled == true { - continue - } - - if runtime.Spec.MultiModel != nil && *runtime.Spec.MultiModel == false { - continue - } - - for _, supportedFormat := range runtime.Spec.SupportedModelFormats { - if supportedFormat.AutoSelect != nil && *supportedFormat.AutoSelect == true && supportedFormat.Name == inferenceservice.Spec.Predictor.Model.ModelFormat.Name { - desiredServingRuntime = runtime - log.Info("Automatic runtime selection for InferenceService", "runtime", desiredServingRuntime.Name) - return &desiredServingRuntime - } - } - } - - log.Info("No suitable Runtime available for InferenceService") - return &desiredServingRuntime - } -} - -// Reconcile will manage the creation, update and deletion of the route returned -// by the newRoute function -func (r *OpenshiftInferenceServiceReconciler) reconcileRoute(inferenceservice *kservev1beta1.InferenceService, - ctx context.Context, newRoute func(service *kservev1beta1.InferenceService, enableAuth bool) *routev1.Route) error { - // Initialize logger format - log := r.Log.WithValues("inferenceservice", inferenceservice.Name, "namespace", inferenceservice.Namespace) - - desiredServingRuntime := r.findSupportingRuntimeForISvc(ctx, log, inferenceservice) - - enableAuth := true - if desiredServingRuntime.Annotations["enable-auth"] != "true" { - enableAuth = false - } - createRoute := true - if desiredServingRuntime.Annotations["enable-route"] != "true" { - createRoute = false - } - - // Generate the desired route - desiredRoute := newRoute(inferenceservice, enableAuth) - - // Create the route if it does not already exist - foundRoute := &routev1.Route{} - justCreated := false - err := r.Get(ctx, types.NamespacedName{ - Name: desiredRoute.Name, - Namespace: inferenceservice.Namespace, - }, foundRoute) - if err != nil { - if !createRoute { - log.Info("Serving runtime does not have 'enable-route' annotation set to 'True'. Skipping route creation") - return nil - } - if apierrs.IsNotFound(err) { - log.Info("Creating Route") - // Add .metatada.ownerReferences to the route to be deleted by the - // Kubernetes garbage collector if the predictor is deleted - err = ctrl.SetControllerReference(inferenceservice, desiredRoute, r.Scheme) - if err != nil { - log.Error(err, "Unable to add OwnerReference to the Route") - return err - } - // Create the route in the Openshift cluster - err = r.Create(ctx, desiredRoute) - if err != nil && !apierrs.IsAlreadyExists(err) { - log.Error(err, "Unable to create the Route") - return err - } - justCreated = true - } else { - log.Error(err, "Unable to fetch the Route") - return err - } - } - - if !createRoute { - log.Info("Serving Runtime does not have 'enable-route' annotation set to 'True'. Deleting existing route") - return r.Delete(ctx, foundRoute) - } - // Reconcile the route spec if it has been manually modified - if !justCreated && !CompareInferenceServiceRoutes(*desiredRoute, *foundRoute) { - log.Info("Reconciling Route") - // Retry the update operation when the ingress controller eventually - // updates the resource version field - err := retry.RetryOnConflict(retry.DefaultRetry, func() error { - // Get the last route revision - if err := r.Get(ctx, types.NamespacedName{ - Name: desiredRoute.Name, - Namespace: inferenceservice.Namespace, - }, foundRoute); err != nil { - return err - } - // Reconcile labels and spec field - foundRoute.Spec = desiredRoute.Spec - foundRoute.ObjectMeta.Labels = desiredRoute.ObjectMeta.Labels - return r.Update(ctx, foundRoute) - }) - if err != nil { - log.Error(err, "Unable to reconcile the Route") - return err - } - } - - return nil -} - -// ReconcileRoute will manage the creation, update and deletion of the -// TLS route when the predictor is reconciled -func (r *OpenshiftInferenceServiceReconciler) ReconcileRoute( - inferenceservice *kservev1beta1.InferenceService, ctx context.Context) error { - return r.reconcileRoute(inferenceservice, ctx, NewInferenceServiceRoute) -} diff --git a/controllers/inferenceservice_virtualservice.go b/controllers/inferenceservice_virtualservice.go deleted file mode 100644 index 1b25e378..00000000 --- a/controllers/inferenceservice_virtualservice.go +++ /dev/null @@ -1,145 +0,0 @@ -/* - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package controllers - -import ( - "context" - "reflect" - - kservev1beta1 "github.com/kserve/kserve/pkg/apis/serving/v1beta1" - "istio.io/api/meta/v1alpha1" - "istio.io/api/networking/v1alpha3" - virtualservicev1 "istio.io/client-go/pkg/apis/networking/v1alpha3" - apierrs "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/util/retry" - ctrl "sigs.k8s.io/controller-runtime" -) - -// NewInferenceServiceVirtualService defines the desired VirtualService object -func NewInferenceServiceVirtualService(inferenceservice *kservev1beta1.InferenceService) *virtualservicev1.VirtualService { - return &virtualservicev1.VirtualService{ - TypeMeta: metav1.TypeMeta{}, - ObjectMeta: metav1.ObjectMeta{Name: inferenceservice.Name, Namespace: inferenceservice.Namespace, Labels: map[string]string{"inferenceservice-name": inferenceservice.Name}}, - Spec: v1alpha3.VirtualService{ - Gateways: []string{"opendatahub/odh-gateway"}, //TODO get actual gateway to be used - Hosts: []string{"*"}, - Http: []*v1alpha3.HTTPRoute{{ - Match: []*v1alpha3.HTTPMatchRequest{{ - Uri: &v1alpha3.StringMatch{ - MatchType: &v1alpha3.StringMatch_Prefix{ - Prefix: "/modelmesh/" + inferenceservice.Namespace + "/", - }, - }, - }}, - Rewrite: &v1alpha3.HTTPRewrite{ - Uri: "/", - }, - Route: []*v1alpha3.HTTPRouteDestination{{ - Destination: &v1alpha3.Destination{ - Host: "modelmesh-serving." + inferenceservice.Namespace + ".svc.cluster.local", - Port: &v1alpha3.PortSelector{ - Number: 8008, - }, - }, - }}, - }}, - }, - Status: v1alpha1.IstioStatus{}, - } -} - -// CompareInferenceServiceVirtualServices checks if two VirtualServices are equal, if not return false -func CompareInferenceServiceVirtualServices(vs1 *virtualservicev1.VirtualService, vs2 *virtualservicev1.VirtualService) bool { - // Two VirtualServices will be equal if the labels and spec are identical - return reflect.DeepEqual(vs1.ObjectMeta.Labels, vs2.ObjectMeta.Labels) && - reflect.DeepEqual(vs1.Spec.Hosts, vs2.Spec.Hosts) -} - -// Reconcile will manage the creation, update and deletion of the VirtualService returned -// by the newVirtualService function -func (r *OpenshiftInferenceServiceReconciler) reconcileVirtualService(inferenceservice *kservev1beta1.InferenceService, - ctx context.Context, newVirtualService func(service *kservev1beta1.InferenceService) *virtualservicev1.VirtualService) error { - // Initialize logger format - log := r.Log.WithValues("inferenceservice", inferenceservice.Name, "namespace", inferenceservice.Namespace) - - // Generate the desired VirtualService - desiredVirtualService := newVirtualService(inferenceservice) - - // Create the VirtualService if it does not already exist - foundVirtualService := &virtualservicev1.VirtualService{} - justCreated := false - err := r.Get(ctx, types.NamespacedName{ - Name: desiredVirtualService.Name, - Namespace: inferenceservice.Namespace, - }, foundVirtualService) - if err != nil { - if apierrs.IsNotFound(err) { - log.Info("Creating VirtualService") - // Add .metatada.ownerReferences to the VirtualService to be deleted by the - // Kubernetes garbage collector if the Predictor is deleted - err = ctrl.SetControllerReference(inferenceservice, desiredVirtualService, r.Scheme) - if err != nil { - log.Error(err, "Unable to add OwnerReference to the VirtualService") - return err - } - // Create the VirtualService in the Openshift cluster - err = r.Create(ctx, desiredVirtualService) - if err != nil && !apierrs.IsAlreadyExists(err) { - log.Error(err, "Unable to create the VirtualService") - return err - } - justCreated = true - } else { - log.Error(err, "Unable to fetch the VirtualService") - return err - } - } - - // Reconcile the VirtualService spec if it has been manually modified - if !justCreated && !CompareInferenceServiceVirtualServices(desiredVirtualService, foundVirtualService) { - log.Info("Reconciling VirtualService") - // Retry the update operation when the ingress controller eventually - // updates the resource version field - err := retry.RetryOnConflict(retry.DefaultRetry, func() error { - // Get the last VirtualService revision - if err := r.Get(ctx, types.NamespacedName{ - Name: desiredVirtualService.Name, - Namespace: inferenceservice.Namespace, - }, foundVirtualService); err != nil { - return err - } - // Reconcile labels and spec field - foundVirtualService.Spec = *desiredVirtualService.Spec.DeepCopy() - foundVirtualService.ObjectMeta.Labels = desiredVirtualService.ObjectMeta.Labels - return r.Update(ctx, foundVirtualService) - }) - if err != nil { - log.Error(err, "Unable to reconcile the VirtualService") - return err - } - } - - return nil -} - -// ReconcileVirtualService will manage the creation, update and deletion of the -// VirtualService when the Predictor is reconciled -func (r *OpenshiftInferenceServiceReconciler) ReconcileVirtualService( - inferenceservice *kservev1beta1.InferenceService, ctx context.Context) error { - return r.reconcileVirtualService(inferenceservice, ctx, NewInferenceServiceVirtualService) -} diff --git a/controllers/kserve_inferenceservice_controller.go b/controllers/kserve_inferenceservice_controller.go deleted file mode 100644 index 31a83045..00000000 --- a/controllers/kserve_inferenceservice_controller.go +++ /dev/null @@ -1,666 +0,0 @@ -/* - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package controllers - -import ( - "context" - kservev1beta1 "github.com/kserve/kserve/pkg/apis/serving/v1beta1" - "github.com/opendatahub-io/odh-model-controller/controllers/components" - "github.com/opendatahub-io/odh-model-controller/controllers/constants" - "github.com/opendatahub-io/odh-model-controller/controllers/reconcilers" - monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" - "istio.io/api/security/v1beta1" - "istio.io/api/telemetry/v1alpha1" - istiotypes "istio.io/api/type/v1beta1" - istiosecv1beta1 "istio.io/client-go/pkg/apis/security/v1beta1" - telemetryv1alpha1 "istio.io/client-go/pkg/apis/telemetry/v1alpha1" - corev1 "k8s.io/api/core/v1" - networkingv1 "k8s.io/api/networking/v1" - k8srbacv1 "k8s.io/api/rbac/v1" - "k8s.io/apimachinery/pkg/api/errors" - apierrs "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/intstr" - maistrav1 "maistra.io/api/core/v1" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -const ( - ServiceMeshLabelKey = "opendatahub.io/service-mesh" - peerAuthenticationName = "default" - networkPolicyName = "allow-from-openshift-monitoring-ns" - userWorkloadMonitoringNS = "openshift-user-workload-monitoring" - serviceMeshMemberRollName = "default" - telemetryName = "enable-prometheus-metrics" - istioServiceMonitorName = "istiod-monitor" - istioPodMonitorName = "istio-proxies-monitor" - clusterPrometheusAccessRoleBinding = "kserve-prometheus-k8s" - clusterPrometheusAccessRole = "kserve-prometheus-k8s" - InferenceSercviceLabelName = "serving.kserve.io/inferenceservice" - TelemetryCRD = "telemetries.telemetry.istio.io" - PeerAuthCRD = "peerauthentications.security.istio.io" -) - -func (r *OpenshiftInferenceServiceReconciler) ensurePrometheusRoleBinding(ctx context.Context, req ctrl.Request, ns string) error { - // Initialize logger format - log := r.Log.WithValues("namespace", ns) - - roleBinding := &k8srbacv1.RoleBinding{} - roleBindingFound := true - err := r.Client.Get(ctx, types.NamespacedName{Name: clusterPrometheusAccessRoleBinding, Namespace: ns}, roleBinding) - if err != nil { - if apierrs.IsNotFound(err) { - roleBindingFound = false - } else { - r.Log.Error(err, "Unable to get RoleBinding") - return err - } - } - if !roleBindingFound { - desiredRoleBinding := &k8srbacv1.RoleBinding{ - ObjectMeta: metav1.ObjectMeta{ - Name: clusterPrometheusAccessRoleBinding, - Namespace: ns, - }, - RoleRef: k8srbacv1.RoleRef{ - APIGroup: "rbac.authorization.k8s.io", - Kind: "ClusterRole", - Name: "kserve-prometheus-k8s", - }, - Subjects: []k8srbacv1.Subject{ - { - Kind: "ServiceAccount", - Name: "prometheus-k8s", - Namespace: "openshift-monitoring", - }, - }, - } - err = r.Client.Create(ctx, desiredRoleBinding) - if err != nil { - r.Log.Error(err, "Unable to create RoleBinding for Prometheus Access") - return err - } - } - log.Info("RoleBinding for Prometheus Access already exists in target namespace ") - return nil -} - -func (r *OpenshiftInferenceServiceReconciler) ensureServiceMeshMemberRollEntry(ctx context.Context, req ctrl.Request, ns string) error { - // Initialize logger format - log := r.Log.WithValues("namespace", ns) - observedServiceMeshMemberRoll := &maistrav1.ServiceMeshMemberRoll{} - err := r.Client.Get(ctx, types.NamespacedName{Name: serviceMeshMemberRollName, Namespace: constants.IstioNamespace}, observedServiceMeshMemberRoll) - if err != nil { - if apierrs.IsNotFound(err) { - log.Error(err, "default ServiceMeshMemberRoll not found in namespace: istio-system") - return err - } - log.Error(err, "Unable to get ServiceMeshMemberRoll in namespace: istio-system") - return err - } - //check if the namespace is already in the list, if it does not exists, append and update - serviceMeshMemberRollEntryExists := false - memberList := observedServiceMeshMemberRoll.Spec.Members - for _, member := range memberList { - if member == ns { - serviceMeshMemberRollEntryExists = true - } - } - if !serviceMeshMemberRollEntryExists { - observedServiceMeshMemberRoll.Spec.Members = append(memberList, ns) - err := r.Client.Update(ctx, observedServiceMeshMemberRoll) - if err != nil { - log.Error(err, "Unable to add namespace to default ServiceMeshMemberRoll") - return err - } - } - log.Info("default ServiceMeshMemberRoll already has the target namespace") - return nil -} - -func (r *OpenshiftInferenceServiceReconciler) createOrUpdateIstioTelemetry(ctx context.Context, req ctrl.Request, ns string) error { - // Initialize logger format - log := r.Log.WithValues("namespace", ns) - - telemetry := &telemetryv1alpha1.Telemetry{} - telemetryFound := true - err := r.Client.Get(ctx, types.NamespacedName{Name: telemetryName, Namespace: ns}, telemetry) - if err != nil { - if apierrs.IsNotFound(err) { - telemetryFound = false - } else { - log.Error(err, "Unable to get Telemetry object") - return err - } - } - if !telemetryFound { - desiredTelemetry := &telemetryv1alpha1.Telemetry{ - ObjectMeta: metav1.ObjectMeta{ - Name: telemetryName, - Namespace: ns, - }, - Spec: v1alpha1.Telemetry{ - Selector: &istiotypes.WorkloadSelector{ - MatchLabels: map[string]string{ - "component": "predictor", - }, - }, - Metrics: []*v1alpha1.Metrics{ - { - Providers: []*v1alpha1.ProviderRef{ - { - Name: "prometheus", - }, - }, - }, - }, - }, - } - err = r.Client.Create(ctx, desiredTelemetry) - if err != nil { - log.Error(err, "Unable to create Telemetry object") - return err - } - } - log.Info("Telemetry already exists in target namespace ") - return nil -} - -func (r *OpenshiftInferenceServiceReconciler) createOrUpdateIstioServiceMonitor(ctx context.Context, req ctrl.Request, ns string) error { - - // Initialize logger format - log := r.Log.WithValues("namespace", ns) - - istioServiceMonitor := &monitoringv1.ServiceMonitor{} - istioServiceMonitorFound := true - err := r.Client.Get(ctx, types.NamespacedName{Name: istioServiceMonitorName, Namespace: ns}, istioServiceMonitor) - if err != nil { - if apierrs.IsNotFound(err) { - istioServiceMonitorFound = false - } else { - log.Error(err, "Unable to get Istio ServiceMonitor") - return err - } - } - if !istioServiceMonitorFound { - desiredServiceMonitor := &monitoringv1.ServiceMonitor{ - ObjectMeta: metav1.ObjectMeta{ - Name: "istiod-monitor", - Namespace: ns, - }, - Spec: monitoringv1.ServiceMonitorSpec{ - Selector: metav1.LabelSelector{ - MatchLabels: map[string]string{ - "istio": "pilot", - }, - }, - TargetLabels: []string{"app"}, - Endpoints: []monitoringv1.Endpoint{ - { - Port: "http-monitoring", - Interval: "30s", - }, - }, - }, - } - err = r.Client.Create(ctx, desiredServiceMonitor) - if err != nil { - log.Error(err, "Unable to create Istio ServiceMonitor") - return err - } - } - log.Info("Istio ServiceMonitor already exists in target namespace ") - return nil -} - -func (r *OpenshiftInferenceServiceReconciler) createOrUpdateIstioPodMonitor(ctx context.Context, req ctrl.Request, ns string) error { - // Initialize logger format - log := r.Log.WithValues("namespace", ns) - - istioPodMonitor := &monitoringv1.PodMonitor{} - istioPodMonitorFound := true - err := r.Client.Get(ctx, types.NamespacedName{Name: istioPodMonitorName, Namespace: ns}, istioPodMonitor) - if err != nil { - if apierrs.IsNotFound(err) { - istioPodMonitorFound = false - } else { - log.Error(err, "Unable to get Istio PodMonitor") - return err - } - } - if !istioPodMonitorFound { - desiredPodMonitor := &monitoringv1.PodMonitor{ - ObjectMeta: metav1.ObjectMeta{ - Name: istioPodMonitorName, - Namespace: ns, - }, - Spec: monitoringv1.PodMonitorSpec{ - Selector: metav1.LabelSelector{ - MatchExpressions: []metav1.LabelSelectorRequirement{ - { - Key: "istio-prometheus-ignore", - Operator: metav1.LabelSelectorOpDoesNotExist, - }, - }, - }, - PodMetricsEndpoints: []monitoringv1.PodMetricsEndpoint{ - { - Path: "/stats/prometheus", - Interval: "30s", - }, - }, - }, - } - err = r.Client.Create(ctx, desiredPodMonitor) - if err != nil { - log.Error(err, "Unable to create Istio PodMonitor") - return err - } - } - log.Info("Istio PodMonitor already exists in target namespace ") - return nil -} - -func (r *OpenshiftInferenceServiceReconciler) createOrUpdatePeerAuth(ctx context.Context, req ctrl.Request, inferenceService *kservev1beta1.InferenceService) error { - // Initialize logger format - log := r.Log.WithValues("namespace", inferenceService.Namespace) - - peerAuthFound := true - peerAuth := &istiosecv1beta1.PeerAuthentication{} - err := r.Client.Get(ctx, types.NamespacedName{Name: peerAuthenticationName, Namespace: inferenceService.Namespace}, peerAuth) - if err != nil { - if apierrs.IsNotFound(err) { - peerAuthFound = false - } else { - log.Error(err, "Unable to get PeerAuthentication") - return err - } - } - - if !peerAuthFound { - desiredPeerAuth := &istiosecv1beta1.PeerAuthentication{ - ObjectMeta: metav1.ObjectMeta{ - Name: peerAuthenticationName, - Namespace: req.Namespace, - }, - Spec: v1beta1.PeerAuthentication{ - Selector: &istiotypes.WorkloadSelector{ - MatchLabels: map[string]string{ - "component": "predictor", - }, - }, - Mtls: &v1beta1.PeerAuthentication_MutualTLS{Mode: 3}, - PortLevelMtls: map[uint32]*v1beta1.PeerAuthentication_MutualTLS{ - 8086: {Mode: 2}, - 3000: {Mode: 2}, - }, - }, - } - err = r.Client.Create(ctx, desiredPeerAuth) - if err != nil { - log.Error(err, "Unable to create PeerAuthentication") - return err - } - } - log.Info("PeerAuth already exists for target namespace ") - return nil -} - -func (r *OpenshiftInferenceServiceReconciler) createOrUpdateNetworkPolicy(ctx context.Context, req ctrl.Request, inferenceService *kservev1beta1.InferenceService) error { - // Initialize logger format - log := r.Log.WithValues("namespace", inferenceService.Namespace) - - networkPolicy := &networkingv1.NetworkPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: networkPolicyName, - Namespace: inferenceService.Namespace, - Labels: map[string]string{ - "app.kubernetes.io/version": "release-v1.9", - "networking.knative.dev/ingress-provider": "istio", - }, - }, - Spec: networkingv1.NetworkPolicySpec{ - PodSelector: metav1.LabelSelector{}, - Ingress: []networkingv1.NetworkPolicyIngressRule{ - { - From: []networkingv1.NetworkPolicyPeer{ - { - NamespaceSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "name": "openshift-user-workload-monitoring", - }, - }, - }, - }, - }, - }, - PolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress}, - }, - } - - if err := r.Client.Create(ctx, networkPolicy); err != nil { - if errors.IsAlreadyExists(err) { - log.Info("NetworkPolicy already exists for target namespace ") - if err := r.Client.Update(ctx, networkPolicy); err != nil { - return err - } - } else { - log.Error(err, "Unable to create Networkpolicy") - return err - } - } - return nil -} - -func (r *OpenshiftInferenceServiceReconciler) createOrUpdateMetricsService(ctx context.Context, req ctrl.Request, inferenceService *kservev1beta1.InferenceService) error { - // Initialize logger format - log := r.Log.WithValues("InferenceService", inferenceService.Name, "namespace", inferenceService.Namespace) - - serviceMetricsName := inferenceService.Name + "-metrics" - - metricsService := &corev1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: serviceMetricsName, - Namespace: inferenceService.Namespace, - Labels: map[string]string{ - "name": serviceMetricsName, - }, - }, - Spec: corev1.ServiceSpec{ - Ports: []corev1.ServicePort{ - { - Name: "caikit-metrics", - Protocol: corev1.ProtocolTCP, - Port: 8086, - TargetPort: intstr.FromInt(8086), - }, - { - Name: "tgis-metrics", - Protocol: corev1.ProtocolTCP, - Port: 3000, - TargetPort: intstr.FromInt(3000), - }, - }, - Type: corev1.ServiceTypeClusterIP, - Selector: map[string]string{ - InferenceSercviceLabelName: inferenceService.Name, - }, - }, - } - err := ctrl.SetControllerReference(inferenceService, metricsService, r.Scheme) - if err != nil { - log.Error(err, "Unable to add OwnerReference to the Metrics Service") - return err - } - if err := r.Client.Create(ctx, metricsService); err != nil { - if errors.IsAlreadyExists(err) { - log.Info("Metrics Service already exists for InferenceService ") - if err := r.Client.Update(ctx, metricsService); err != nil { - return err - } - } else { - log.Error(err, "Unable to create Metrics Service") - return err - } - } - return nil -} - -func (r *OpenshiftInferenceServiceReconciler) createOrUpdateMetricsServiceMonitor(ctx context.Context, req ctrl.Request, inferenceService *kservev1beta1.InferenceService) error { - // Initialize logger format - log := r.Log.WithValues("InferenceService", inferenceService.Name, "namespace", inferenceService.Namespace) - - serviceMetricsName := inferenceService.Name + "-metrics" - - metricsServiceMonitor := &monitoringv1.ServiceMonitor{ - ObjectMeta: metav1.ObjectMeta{ - Name: serviceMetricsName, - Namespace: inferenceService.Namespace, - }, - Spec: monitoringv1.ServiceMonitorSpec{ - Endpoints: []monitoringv1.Endpoint{ - { - Port: "caikit-metrics", - Scheme: "http", - }, - { - Port: "tgis-metrics", - Scheme: "http", - }, - }, - NamespaceSelector: monitoringv1.NamespaceSelector{}, - Selector: metav1.LabelSelector{ - MatchLabels: map[string]string{ - "name": serviceMetricsName, - }, - }, - }, - } - err := ctrl.SetControllerReference(inferenceService, metricsServiceMonitor, r.Scheme) - if err != nil { - log.Error(err, "Unable to add OwnerReference to the Metrics ServiceMonitor") - return err - } - if err = r.Client.Create(ctx, metricsServiceMonitor); err != nil { - if errors.IsAlreadyExists(err) { - log.Info("Metrics ServiceMonitor already exists for InferenceService") - if err := r.Client.Update(ctx, metricsServiceMonitor); err != nil { - return err - } - } else { - log.Error(err, "Unable to create Metrics ServiceMonitor") - return err - } - } - return nil -} - -func (r *OpenshiftInferenceServiceReconciler) OnDeletionOfKserveInferenceService(ctx context.Context, inferenceService *kservev1beta1.InferenceService) error { - log := r.Log.WithValues("InferenceService", inferenceService.Name, "namespace", inferenceService.Namespace) - - log.V(1).Info("Deleting Kserve inference service generic route") - routeHandler := components.NewRouteHandler(r.Client, ctx, log) - return routeHandler.DeleteRoute(types.NamespacedName{Name: reconcilers.GetKServeRouteName(inferenceService), Namespace: constants.IstioNamespace}) -} - -func (r *OpenshiftInferenceServiceReconciler) DeleteKserveMetricsResourcesIfNoKserveIsvcExists(ctx context.Context, req ctrl.Request, ns string) error { - // Initialize logger format - log := r.Log.WithValues("namespace", ns) - - inferenceServiceList := &kservev1beta1.InferenceServiceList{} - err := r.List(ctx, inferenceServiceList, client.InNamespace(req.Namespace)) - if err != nil { - log.Error(err, "Unable to get the list of InferenceServices in the namespace. Cannot delete PeerAuthentication and NetworkPolicy.") - return err - } - - for i := len(inferenceServiceList.Items) - 1; i >= 0; i-- { - inferenceService := inferenceServiceList.Items[i] - if r.isDeploymentModeForIsvcModelMesh(&inferenceService) { - inferenceServiceList.Items = append(inferenceServiceList.Items[:i], inferenceServiceList.Items[i+1:]...) - } - } - - // If there are no Kserve InferenceServices in the namespace, delete namespace-scoped resources needed for Kserve Metrics - if len(inferenceServiceList.Items) == 0 { - peerAuth := &istiosecv1beta1.PeerAuthentication{ - ObjectMeta: metav1.ObjectMeta{ - Name: peerAuthenticationName, - Namespace: req.Namespace, - }, - } - - err := r.Delete(ctx, peerAuth) - if err != nil && !apierrs.IsNotFound(err) { - log.Error(err, "No Kserve InferenceServices exist in the namespace. Unable to delete PeerAuthentication") - return err - } - - networkPolicy := &networkingv1.NetworkPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: networkPolicyName, - Namespace: req.Namespace, - }, - } - - err = r.Delete(ctx, networkPolicy) - if err != nil && !apierrs.IsNotFound(err) { - log.Error(err, "No Kserve InferenceServices exist in the namespace. Unable to delete NetworkPolicy") - return err - } - - serviceMeshMemberRoll := &maistrav1.ServiceMeshMemberRoll{} - err = r.Client.Get(ctx, types.NamespacedName{Name: serviceMeshMemberRollName, Namespace: constants.IstioNamespace}, serviceMeshMemberRoll) - if err != nil { - log.Error(err, "Failed to get ServiceMeshMemberRoll.") - return err - } - serviceMeshMemberList := serviceMeshMemberRoll.Spec.Members - // remove the namespace from the list - for i, member := range serviceMeshMemberList { - if member == req.Namespace { - serviceMeshMemberList = append(serviceMeshMemberList[:i], serviceMeshMemberList[i+1:]...) - } - } - serviceMeshMemberRoll.Spec.Members = serviceMeshMemberList - err = r.Client.Update(ctx, serviceMeshMemberRoll) - if err != nil { - log.Error(err, "No Kserve InferenceServices exist in the namespace. Unable to delete namespace from default ServiceMeshMemberRoll.") - return err - } - - telemetry := &telemetryv1alpha1.Telemetry{ - ObjectMeta: metav1.ObjectMeta{ - Name: telemetryName, - Namespace: req.Namespace, - }, - } - err = r.Delete(ctx, telemetry) - if err != nil && !apierrs.IsNotFound(err) { - log.Error(err, "No Kserve InferenceServices exist in the namespace. Unable to delete Telemetry object.") - return err - } - - istioServiceMonitor := &monitoringv1.ServiceMonitor{ - ObjectMeta: metav1.ObjectMeta{ - Name: istioServiceMonitorName, - Namespace: req.Namespace, - }, - } - err = r.Delete(ctx, istioServiceMonitor) - if err != nil && !apierrs.IsNotFound(err) { - log.Error(err, "No Kserve InferenceServices exist in the namespace. Unable to delete Istio ServiceMonitor.") - return err - } - - istioPodMonitor := &monitoringv1.PodMonitor{ - ObjectMeta: metav1.ObjectMeta{ - Name: istioPodMonitorName, - Namespace: req.Namespace, - }, - } - err = r.Delete(ctx, istioPodMonitor) - if err != nil && !apierrs.IsNotFound(err) { - log.Error(err, "No Kserve InferenceServices exist in the namespace. Unable to delete Istio PodMonitor.") - return err - } - - prometheusRoleBinding := &k8srbacv1.RoleBinding{ - ObjectMeta: metav1.ObjectMeta{ - Name: clusterPrometheusAccessRoleBinding, - Namespace: req.Namespace, - }, - } - err = r.Delete(ctx, prometheusRoleBinding) - if err != nil && !apierrs.IsNotFound(err) { - log.Error(err, "No Kserve InferenceServices exist in the namespace. Unable to delete RoleBinding for Prometheus Access.") - return err - } - } - return nil -} - -func (r *OpenshiftInferenceServiceReconciler) ReconcileKserveInference(ctx context.Context, req ctrl.Request, inferenceService *kservev1beta1.InferenceService) error { - - // Initialize logger format - log := r.Log.WithValues("InferenceService", inferenceService.Name, "namespace", inferenceService.Namespace) - - log.Info("Verifying that the default ServiceMeshMemberRoll has the target namespace") - err := r.ensureServiceMeshMemberRollEntry(ctx, req, inferenceService.Namespace) - if err != nil { - return err - } - - log.Info("Reconciling Generic Route for Kserve InferenceService") - kisvcRouteReconciler := reconcilers.NewKserveInferenceServiceRouteReconciler(r.Client, r.Scheme, ctx, log, inferenceService) - err = kisvcRouteReconciler.Reconcile() - if err != nil { - return err - } - - //Create the metrics service and servicemonitor with OwnerReferences, as these are not common namespace-scope resources - log.Info("Reconciling Metrics Service for InferenceSercvice") - err = r.createOrUpdateMetricsService(ctx, req, inferenceService) - if err != nil { - return err - } - - log.Info("Reconciling Metrics ServiceMonitor for InferenceSercvice") - err = r.createOrUpdateMetricsServiceMonitor(ctx, req, inferenceService) - if err != nil { - return err - } - - log.Info("Verifying that the rolebinding to enable prometheus access exists") - err = r.ensurePrometheusRoleBinding(ctx, req, inferenceService.Namespace) - if err != nil { - return err - } - - log.Info("Creating Istio Telemetry object for target namespace") - err = r.createOrUpdateIstioTelemetry(ctx, req, inferenceService.Namespace) - if err != nil { - return err - } - - log.Info("Creating Istio ServiceMonitor for target namespace") - err = r.createOrUpdateIstioServiceMonitor(ctx, req, inferenceService.Namespace) - if err != nil { - return err - } - - log.Info("Creating Istio PodMonitor for target namespace") - err = r.createOrUpdateIstioPodMonitor(ctx, req, inferenceService.Namespace) - if err != nil { - return err - } - - log.Info("Reconciling PeerAuthentication for target namespace") - err = r.createOrUpdatePeerAuth(ctx, req, inferenceService) - if err != nil { - return err - } - - log.Info("Reconciling NetworkPolicy for target namespace") - err = r.createOrUpdateNetworkPolicy(ctx, req, inferenceService) - if err != nil { - return err - } - - return nil -} diff --git a/controllers/kserve_inferenceservice_controller_test.go b/controllers/kserve_inferenceservice_controller_test.go index 8bef7b82..ecd5a6c9 100644 --- a/controllers/kserve_inferenceservice_controller_test.go +++ b/controllers/kserve_inferenceservice_controller_test.go @@ -22,20 +22,14 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "github.com/opendatahub-io/odh-model-controller/controllers/constants" - "github.com/opendatahub-io/odh-model-controller/controllers/reconcilers" routev1 "github.com/openshift/api/route/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "knative.dev/pkg/apis" - v1 "maistra.io/api/core/v1" "time" ) -const ( - InferenceServiceConfigPath1 = "./testdata/configmaps/inferenceservice-config.yaml" -) - var _ = Describe("The Openshift Kserve model controller", func() { When("creating a Kserve ServiceRuntime & InferenceService", func() { @@ -49,14 +43,6 @@ var _ = Describe("The Openshift Kserve model controller", func() { } Expect(cli.Create(ctx, istioNamespace)).Should(Succeed()) - smmr := &v1.ServiceMeshMemberRoll{ - ObjectMeta: metav1.ObjectMeta{ - Name: serviceMeshMemberRollName, - Namespace: constants.IstioNamespace, - }, - } - Expect(cli.Create(ctx, smmr)).Should(Succeed()) - inferenceServiceConfig := &corev1.ConfigMap{} err := convertToStructuredResource(InferenceServiceConfigPath1, inferenceServiceConfig) Expect(err).NotTo(HaveOccurred()) @@ -79,7 +65,7 @@ var _ = Describe("The Openshift Kserve model controller", func() { //time.Sleep(5000 * time.Millisecond) Consistently(func() error { route := &routev1.Route{} - key := types.NamespacedName{Name: reconcilers.GetKServeRouteName(inferenceService), Namespace: constants.IstioNamespace} + key := types.NamespacedName{Name: getKServeRouteName(inferenceService), Namespace: constants.IstioNamespace} err = cli.Get(ctx, key, route) return err }, time.Second*1, interval).Should(HaveOccurred()) @@ -98,10 +84,14 @@ var _ = Describe("The Openshift Kserve model controller", func() { By("By checking that the controller has created the Route") Eventually(func() error { route := &routev1.Route{} - key := types.NamespacedName{Name: reconcilers.GetKServeRouteName(inferenceService), Namespace: constants.IstioNamespace} + key := types.NamespacedName{Name: getKServeRouteName(inferenceService), Namespace: constants.IstioNamespace} err = cli.Get(ctx, key, route) return err }, timeout, interval).ShouldNot(HaveOccurred()) }) }) }) + +func getKServeRouteName(isvc *kservev1beta1.InferenceService) string { + return isvc.Name + "-" + isvc.Namespace +} diff --git a/controllers/mm_inferenceservice_controller.go b/controllers/mm_inferenceservice_controller.go deleted file mode 100644 index afa3dd82..00000000 --- a/controllers/mm_inferenceservice_controller.go +++ /dev/null @@ -1,25 +0,0 @@ -package controllers - -import ( - "context" - kservev1beta1 "github.com/kserve/kserve/pkg/apis/serving/v1beta1" - ctrl "sigs.k8s.io/controller-runtime" -) - -func (r *OpenshiftInferenceServiceReconciler) ReconcileModelMeshInference(ctx context.Context, req ctrl.Request, inferenceService *kservev1beta1.InferenceService) error { - // Initialize logger format - log := r.Log.WithValues("InferenceService", inferenceService.Name, "namespace", inferenceService.Namespace) - - log.Info("Reconciling Route for InferenceService") - err := r.ReconcileRoute(inferenceService, ctx) - if err != nil { - return err - } - - log.Info("Reconciling ServiceAccount for InferenceSercvice") - err = r.ReconcileSA(inferenceService, ctx) - if err != nil { - return err - } - return nil -} diff --git a/controllers/inferenceservice_controller_test.go b/controllers/mm_inferenceservice_controller_test.go similarity index 92% rename from controllers/inferenceservice_controller_test.go rename to controllers/mm_inferenceservice_controller_test.go index eab31f7b..75d0cc55 100644 --- a/controllers/inferenceservice_controller_test.go +++ b/controllers/mm_inferenceservice_controller_test.go @@ -19,6 +19,7 @@ import ( "context" kservev1alpha1 "github.com/kserve/kserve/pkg/apis/serving/v1alpha1" kservev1beta1 "github.com/kserve/kserve/pkg/apis/serving/v1beta1" + "github.com/opendatahub-io/odh-model-controller/controllers/comparators" routev1 "github.com/openshift/api/route/v1" "k8s.io/apimachinery/pkg/types" @@ -62,7 +63,7 @@ var _ = Describe("The Openshift model controller", func() { err = convertToStructuredResource(ExpectedRoutePath, expectedRoute) Expect(err).NotTo(HaveOccurred()) - Expect(CompareInferenceServiceRoutes(*route, *expectedRoute)).Should(BeTrue()) + Expect(comparators.GetMMRouteComparator()(route, expectedRoute)).Should(BeTrue()) }) It("when InferenceService does not specifies a runtime, should automatically pick a runtime and create a Route", func() { @@ -81,7 +82,7 @@ var _ = Describe("The Openshift model controller", func() { err = convertToStructuredResource(ExpectedRouteNoRuntimePath, expectedRoute) Expect(err).NotTo(HaveOccurred()) - Expect(CompareInferenceServiceRoutes(*route, *expectedRoute)).Should(BeTrue()) + Expect(comparators.GetMMRouteComparator()(route, expectedRoute)).Should(BeTrue()) }) }) }) diff --git a/controllers/processors/deltaProcessor.go b/controllers/processors/deltaProcessor.go index 216de9d0..a399972c 100644 --- a/controllers/processors/deltaProcessor.go +++ b/controllers/processors/deltaProcessor.go @@ -1,3 +1,18 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package processors import ( @@ -18,17 +33,19 @@ func NewDeltaProcessor() DeltaProcessor { return &deltaProcessor{} } -func (d *deltaProcessor) ComputeDelta(comparator comparators.ResourceComparator, requestedResource client.Object, deployedResource client.Object) ResourceDelta { +func (d *deltaProcessor) ComputeDelta(comparator comparators.ResourceComparator, desiredResource client.Object, existingResource client.Object) ResourceDelta { var added bool var updated bool var removed bool - if utils.IsNotNil(requestedResource) && utils.IsNil(deployedResource) { - added = true - } else if utils.IsNil(requestedResource) && utils.IsNotNil(deployedResource) { - removed = true - } else if !comparator.Compare(deployedResource, requestedResource) { - updated = true + if !utils.IsNil(desiredResource) && utils.IsNil(existingResource) { + if utils.IsNotNil(desiredResource) && utils.IsNil(existingResource) { + added = true + } else if utils.IsNil(desiredResource) && utils.IsNotNil(existingResource) { + removed = true + } else if !comparator(existingResource, desiredResource) { + updated = true + } } return &resourceDelta{ diff --git a/controllers/reconcilers/kserve_inferenceservice_reconciler.go b/controllers/reconcilers/kserve_inferenceservice_reconciler.go new file mode 100644 index 00000000..e2a698cc --- /dev/null +++ b/controllers/reconcilers/kserve_inferenceservice_reconciler.go @@ -0,0 +1,171 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package reconcilers + +import ( + "context" + "github.com/go-logr/logr" + kservev1beta1 "github.com/kserve/kserve/pkg/apis/serving/v1beta1" + "github.com/opendatahub-io/odh-model-controller/controllers/utils" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type KserveInferenceServiceReconciler struct { + client client.Client + routeReconciler *KserveRouteReconciler + metricsServiceReconciler *KserveMetricsServiceReconciler + metricsServiceMonitorReconciler *KserveMetricsServiceMonitorReconciler + prometheusRoleBindingReconciler *KservePrometheusRoleBindingReconciler + istioSMMRReconciler *KserveIstioSMMRReconciler + istioTelemetryReconciler *KserveIstioTelemetryReconciler + istioServiceMonitorReconciler *KserveIstioServiceMonitorReconciler + istioPodMonitorReconciler *KserveIstioPodMonitorReconciler + istioPeerAuthenticationReconciler *KserveIstioPeerAuthenticationReconciler + networkPolicyReconciler *KserveNetworkPolicyReconciler +} + +func NewKServeInferenceServiceReconciler(client client.Client, scheme *runtime.Scheme) *KserveInferenceServiceReconciler { + return &KserveInferenceServiceReconciler{ + client: client, + istioSMMRReconciler: NewKServeIstioSMMRReconciler(client, scheme), + routeReconciler: NewKserveRouteReconciler(client, scheme), + metricsServiceReconciler: NewKServeMetricsServiceReconciler(client, scheme), + metricsServiceMonitorReconciler: NewKServeMetricsServiceMonitorReconciler(client, scheme), + prometheusRoleBindingReconciler: NewKServePrometheusRoleBindingReconciler(client, scheme), + istioTelemetryReconciler: NewKServeIstioTelemetryReconciler(client, scheme), + istioServiceMonitorReconciler: NewKServeIstioServiceMonitorReconciler(client, scheme), + istioPodMonitorReconciler: NewKServeIstioPodMonitorReconciler(client, scheme), + istioPeerAuthenticationReconciler: NewKServeIstioPeerAuthenticationReconciler(client, scheme), + networkPolicyReconciler: NewKServeNetworkPolicyReconciler(client, scheme), + } +} + +func (r *KserveInferenceServiceReconciler) Reconcile(ctx context.Context, log logr.Logger, isvc *kservev1beta1.InferenceService) error { + + // Resource created per namespace + log.V(1).Info("Verifying that the default ServiceMeshMemberRoll has the target namespace") + if err := r.istioSMMRReconciler.Reconcile(ctx, log, isvc); err != nil { + return err + } + + log.V(1).Info("Verifying that the role binding to enable prometheus access exists") + if err := r.prometheusRoleBindingReconciler.Reconcile(ctx, log, isvc); err != nil { + return err + } + + log.V(1).Info("Creating Istio Telemetry object for target namespace") + if err := r.istioTelemetryReconciler.Reconcile(ctx, log, isvc); err != nil { + return err + } + + log.V(1).Info("Creating Istio ServiceMonitor for target namespace") + if err := r.istioServiceMonitorReconciler.Reconcile(ctx, log, isvc); err != nil { + return err + } + + log.V(1).Info("Creating Istio PodMonitor for target namespace") + if err := r.istioPodMonitorReconciler.Reconcile(ctx, log, isvc); err != nil { + return err + } + + log.V(1).Info("Reconciling PeerAuthentication for target namespace") + if err := r.istioPeerAuthenticationReconciler.Reconcile(ctx, log, isvc); err != nil { + return err + } + + log.V(1).Info("Reconciling NetworkPolicy for target namespace") + if err := r.networkPolicyReconciler.Reconcile(ctx, log, isvc); err != nil { + return err + } + + // Resource created for each ISVC resource + log.V(1).Info("Reconciling Generic Route for Kserve InferenceService") + if err := r.routeReconciler.Reconcile(ctx, log, isvc); err != nil { + return err + } + + log.V(1).Info("Reconciling Metrics Service for InferenceService") + if err := r.metricsServiceReconciler.Reconcile(ctx, log, isvc); err != nil { + return err + } + + log.V(1).Info("Reconciling Metrics ServiceMonitor for InferenceService") + if err := r.metricsServiceMonitorReconciler.Reconcile(ctx, log, isvc); err != nil { + return err + } + + return nil +} + +func (r *KserveInferenceServiceReconciler) OnDeletionOfKserveInferenceService(ctx context.Context, log logr.Logger, isvc *kservev1beta1.InferenceService) error { + log.V(1).Info("Deleting Kserve inference service generic route") + return r.routeReconciler.DeleteRoute(ctx, isvc) +} + +func (r *KserveInferenceServiceReconciler) DeleteKserveMetricsResourcesIfNoKserveIsvcExists(ctx context.Context, log logr.Logger, isvcNamespace string) error { + inferenceServiceList := &kservev1beta1.InferenceServiceList{} + if err := r.client.List(ctx, inferenceServiceList, client.InNamespace(isvcNamespace)); err != nil { + return err + } + + for i := len(inferenceServiceList.Items) - 1; i >= 0; i-- { + inferenceService := inferenceServiceList.Items[i] + if utils.IsDeploymentModeForIsvcModelMesh(&inferenceService) { + inferenceServiceList.Items = append(inferenceServiceList.Items[:i], inferenceServiceList.Items[i+1:]...) + } + } + + // If there are no Kserve InferenceServices in the namespace, delete namespace-scoped resources needed for Kserve Metrics + if len(inferenceServiceList.Items) == 0 { + + log.V(1).Info("Removing target namespace from ServiceMeshMemberRole") + if err := r.istioSMMRReconciler.RemoveMemberFromSMMR(ctx, isvcNamespace); err != nil { + return err + } + + log.V(1).Info("Deleting Prometheus RoleBinding object for target namespace") + if err := r.prometheusRoleBindingReconciler.DeleteRoleBinding(ctx, isvcNamespace); err != nil { + return err + } + + log.V(1).Info("Deleting Istio Telemetry object for target namespace") + if err := r.istioTelemetryReconciler.DeleteTelemetry(ctx, isvcNamespace); err != nil { + return err + } + + log.V(1).Info("Deleting ServiceMonitor object for target namespace") + if err := r.istioServiceMonitorReconciler.DeleteServiceMonitor(ctx, isvcNamespace); err != nil { + return err + } + + log.V(1).Info("Deleting PodMonitor object for target namespace") + if err := r.istioPodMonitorReconciler.DeletePodMonitor(ctx, isvcNamespace); err != nil { + return err + } + + log.V(1).Info("Deleting PeerAuthentication object for target namespace") + if err := r.istioPeerAuthenticationReconciler.DeletePeerAuthentication(ctx, isvcNamespace); err != nil { + return err + } + + log.V(1).Info("Deleting NetworkPolicy object for target namespace") + if err := r.networkPolicyReconciler.DeleteNetworkPolicy(ctx, isvcNamespace); err != nil { + return err + } + } + return nil +} diff --git a/controllers/reconcilers/kserve_istio_peerauthentication_reconciler.go b/controllers/reconcilers/kserve_istio_peerauthentication_reconciler.go new file mode 100644 index 00000000..7390189a --- /dev/null +++ b/controllers/reconcilers/kserve_istio_peerauthentication_reconciler.go @@ -0,0 +1,138 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package reconcilers + +import ( + "context" + "github.com/go-logr/logr" + kservev1beta1 "github.com/kserve/kserve/pkg/apis/serving/v1beta1" + "github.com/opendatahub-io/odh-model-controller/controllers/comparators" + "github.com/opendatahub-io/odh-model-controller/controllers/processors" + "github.com/opendatahub-io/odh-model-controller/controllers/resources" + "istio.io/api/security/v1beta1" + istiotypes "istio.io/api/type/v1beta1" + istiosecv1beta1 "istio.io/client-go/pkg/apis/security/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + peerAuthenticationName = "default" +) + +type KserveIstioPeerAuthenticationReconciler struct { + client client.Client + scheme *runtime.Scheme + peerAuthenticationHandler resources.PeerAuthenticationHandler + deltaProcessor processors.DeltaProcessor +} + +func NewKServeIstioPeerAuthenticationReconciler(client client.Client, scheme *runtime.Scheme) *KserveIstioPeerAuthenticationReconciler { + return &KserveIstioPeerAuthenticationReconciler{ + client: client, + scheme: scheme, + peerAuthenticationHandler: resources.NewPeerAuthenticationHandler(client), + deltaProcessor: processors.NewDeltaProcessor(), + } +} + +func (r *KserveIstioPeerAuthenticationReconciler) Reconcile(ctx context.Context, log logr.Logger, isvc *kservev1beta1.InferenceService) error { + + // Create Desired resource + desiredResource, err := r.createDesiredResource(isvc) + if err != nil { + return err + } + + // Get Existing resource + existingResource, err := r.getExistingResource(ctx, log, isvc) + if err != nil { + return err + } + + // Process Delta + if err = r.processDelta(ctx, log, desiredResource, existingResource); err != nil { + return err + } + return nil +} + +func (r *KserveIstioPeerAuthenticationReconciler) createDesiredResource(isvc *kservev1beta1.InferenceService) (*istiosecv1beta1.PeerAuthentication, error) { + desiredPeerAuthentication := &istiosecv1beta1.PeerAuthentication{ + ObjectMeta: metav1.ObjectMeta{ + Name: peerAuthenticationName, + Namespace: isvc.Namespace, + }, + Spec: v1beta1.PeerAuthentication{ + Selector: &istiotypes.WorkloadSelector{ + MatchLabels: map[string]string{ + "component": "predictor", + }, + }, + Mtls: &v1beta1.PeerAuthentication_MutualTLS{Mode: 3}, + PortLevelMtls: map[uint32]*v1beta1.PeerAuthentication_MutualTLS{ + 8086: {Mode: 2}, + 3000: {Mode: 2}, + }, + }, + } + return desiredPeerAuthentication, nil +} + +func (r *KserveIstioPeerAuthenticationReconciler) getExistingResource(ctx context.Context, log logr.Logger, isvc *kservev1beta1.InferenceService) (*istiosecv1beta1.PeerAuthentication, error) { + return r.peerAuthenticationHandler.FetchPeerAuthentication(ctx, log, types.NamespacedName{Name: peerAuthenticationName, Namespace: isvc.Namespace}) +} + +func (r *KserveIstioPeerAuthenticationReconciler) processDelta(ctx context.Context, log logr.Logger, desiredPod *istiosecv1beta1.PeerAuthentication, existingPod *istiosecv1beta1.PeerAuthentication) (err error) { + comparator := comparators.GetPeerAuthenticationComparator() + delta := r.deltaProcessor.ComputeDelta(comparator, desiredPod, existingPod) + + if !delta.HasChanges() { + log.V(1).Info("No delta found") + return nil + } + + if delta.IsAdded() { + log.V(1).Info("Delta found", "create", desiredPod.GetName()) + if err = r.client.Create(ctx, desiredPod); err != nil { + return + } + } + if delta.IsUpdated() { + log.V(1).Info("Delta found", "update", existingPod.GetName()) + rp := existingPod.DeepCopy() + rp.Spec.Selector = desiredPod.Spec.Selector + rp.Spec.Mtls = desiredPod.Spec.Mtls + rp.Spec.PortLevelMtls = desiredPod.Spec.PortLevelMtls + + if err = r.client.Update(ctx, rp); err != nil { + return + } + } + if delta.IsRemoved() { + log.V(1).Info("Delta found", "delete", existingPod.GetName()) + if err = r.client.Delete(ctx, existingPod); err != nil { + return + } + } + return nil +} + +func (r *KserveIstioPeerAuthenticationReconciler) DeletePeerAuthentication(ctx context.Context, isvcNamespace string) error { + return r.peerAuthenticationHandler.DeletePeerAuthentication(ctx, types.NamespacedName{Name: peerAuthenticationName, Namespace: isvcNamespace}) +} diff --git a/controllers/reconcilers/kserve_istio_podmonitor_reconciler.go b/controllers/reconcilers/kserve_istio_podmonitor_reconciler.go new file mode 100644 index 00000000..895d7e33 --- /dev/null +++ b/controllers/reconcilers/kserve_istio_podmonitor_reconciler.go @@ -0,0 +1,138 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package reconcilers + +import ( + "context" + "github.com/go-logr/logr" + kservev1beta1 "github.com/kserve/kserve/pkg/apis/serving/v1beta1" + "github.com/opendatahub-io/odh-model-controller/controllers/comparators" + "github.com/opendatahub-io/odh-model-controller/controllers/processors" + "github.com/opendatahub-io/odh-model-controller/controllers/resources" + v1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + istioPodMonitorName = "istio-proxies-monitor" +) + +type KserveIstioPodMonitorReconciler struct { + client client.Client + scheme *runtime.Scheme + podMonitorHandler resources.PodMonitorHandler + deltaProcessor processors.DeltaProcessor +} + +func NewKServeIstioPodMonitorReconciler(client client.Client, scheme *runtime.Scheme) *KserveIstioPodMonitorReconciler { + return &KserveIstioPodMonitorReconciler{ + client: client, + scheme: scheme, + podMonitorHandler: resources.NewPodMonitorHandler(client), + deltaProcessor: processors.NewDeltaProcessor(), + } +} + +func (r *KserveIstioPodMonitorReconciler) Reconcile(ctx context.Context, log logr.Logger, isvc *kservev1beta1.InferenceService) error { + + // Create Desired resource + desiredResource, err := r.createDesiredResource(isvc) + if err != nil { + return err + } + + // Get Existing resource + existingResource, err := r.getExistingResource(ctx, log, isvc) + if err != nil { + return err + } + + // Process Delta + if err = r.processDelta(ctx, log, desiredResource, existingResource); err != nil { + return err + } + return nil +} + +func (r *KserveIstioPodMonitorReconciler) createDesiredResource(isvc *kservev1beta1.InferenceService) (*v1.PodMonitor, error) { + desiredPodMonitor := &v1.PodMonitor{ + ObjectMeta: metav1.ObjectMeta{ + Name: istioPodMonitorName, + Namespace: isvc.Namespace, + }, + Spec: v1.PodMonitorSpec{ + Selector: metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "istio-prometheus-ignore", + Operator: metav1.LabelSelectorOpDoesNotExist, + }, + }, + }, + PodMetricsEndpoints: []v1.PodMetricsEndpoint{ + { + Path: "/stats/prometheus", + Interval: "30s", + }, + }, + }, + } + return desiredPodMonitor, nil +} + +func (r *KserveIstioPodMonitorReconciler) getExistingResource(ctx context.Context, log logr.Logger, isvc *kservev1beta1.InferenceService) (*v1.PodMonitor, error) { + return r.podMonitorHandler.FetchPodMonitor(ctx, log, types.NamespacedName{Name: istioPodMonitorName, Namespace: isvc.Namespace}) +} + +func (r *KserveIstioPodMonitorReconciler) processDelta(ctx context.Context, log logr.Logger, desiredPod *v1.PodMonitor, existingPod *v1.PodMonitor) (err error) { + comparator := comparators.GetPodMonitorComparator() + delta := r.deltaProcessor.ComputeDelta(comparator, desiredPod, existingPod) + + if !delta.HasChanges() { + log.V(1).Info("No delta found") + return nil + } + + if delta.IsAdded() { + log.V(1).Info("Delta found", "create", desiredPod.GetName()) + if err = r.client.Create(ctx, desiredPod); err != nil { + return + } + } + if delta.IsUpdated() { + log.V(1).Info("Delta found", "update", existingPod.GetName()) + rp := existingPod.DeepCopy() + rp.Spec = desiredPod.Spec + + if err = r.client.Update(ctx, rp); err != nil { + return + } + } + if delta.IsRemoved() { + log.V(1).Info("Delta found", "delete", existingPod.GetName()) + if err = r.client.Delete(ctx, existingPod); err != nil { + return + } + } + return nil +} + +func (r *KserveIstioPodMonitorReconciler) DeletePodMonitor(ctx context.Context, isvcNamespace string) error { + return r.podMonitorHandler.DeletePodMonitor(ctx, types.NamespacedName{Name: istioPodMonitorName, Namespace: isvcNamespace}) +} diff --git a/controllers/reconcilers/kserve_istio_servicemonitor_reconciler.go b/controllers/reconcilers/kserve_istio_servicemonitor_reconciler.go new file mode 100644 index 00000000..029b5231 --- /dev/null +++ b/controllers/reconcilers/kserve_istio_servicemonitor_reconciler.go @@ -0,0 +1,138 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package reconcilers + +import ( + "context" + "github.com/go-logr/logr" + kservev1beta1 "github.com/kserve/kserve/pkg/apis/serving/v1beta1" + "github.com/opendatahub-io/odh-model-controller/controllers/comparators" + "github.com/opendatahub-io/odh-model-controller/controllers/processors" + "github.com/opendatahub-io/odh-model-controller/controllers/resources" + v1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + istioServiceMonitorName = "istiod-monitor" +) + +type KserveIstioServiceMonitorReconciler struct { + client client.Client + scheme *runtime.Scheme + serviceMonitorHandler resources.ServiceMonitorHandler + deltaProcessor processors.DeltaProcessor +} + +func NewKServeIstioServiceMonitorReconciler(client client.Client, scheme *runtime.Scheme) *KserveIstioServiceMonitorReconciler { + return &KserveIstioServiceMonitorReconciler{ + client: client, + scheme: scheme, + serviceMonitorHandler: resources.NewServiceMonitorHandler(client), + deltaProcessor: processors.NewDeltaProcessor(), + } +} + +func (r *KserveIstioServiceMonitorReconciler) Reconcile(ctx context.Context, log logr.Logger, isvc *kservev1beta1.InferenceService) error { + + // Create Desired resource + desiredResource, err := r.createDesiredResource(isvc) + if err != nil { + return err + } + + // Get Existing resource + existingResource, err := r.getExistingResource(ctx, log, isvc) + if err != nil { + return err + } + + // Process Delta + if err = r.processDelta(ctx, log, desiredResource, existingResource); err != nil { + return err + } + return nil +} + +func (r *KserveIstioServiceMonitorReconciler) createDesiredResource(isvc *kservev1beta1.InferenceService) (*v1.ServiceMonitor, error) { + desiredServiceMonitor := &v1.ServiceMonitor{ + ObjectMeta: metav1.ObjectMeta{ + Name: istioServiceMonitorName, + Namespace: isvc.Namespace, + }, + Spec: v1.ServiceMonitorSpec{ + Selector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "istio": "pilot", + }, + }, + TargetLabels: []string{"app"}, + Endpoints: []v1.Endpoint{ + { + Port: "http-monitoring", + Interval: "30s", + }, + }, + }, + } + return desiredServiceMonitor, nil +} + +func (r *KserveIstioServiceMonitorReconciler) getExistingResource(ctx context.Context, log logr.Logger, isvc *kservev1beta1.InferenceService) (*v1.ServiceMonitor, error) { + return r.serviceMonitorHandler.FetchServiceMonitor(ctx, log, types.NamespacedName{Name: istioServiceMonitorName, Namespace: isvc.Namespace}) +} + +func (r *KserveIstioServiceMonitorReconciler) processDelta(ctx context.Context, log logr.Logger, desiredService *v1.ServiceMonitor, existingService *v1.ServiceMonitor) (err error) { + comparator := comparators.GetServiceMonitorComparator() + delta := r.deltaProcessor.ComputeDelta(comparator, desiredService, existingService) + + if !delta.HasChanges() { + log.V(1).Info("No delta found") + return nil + } + + if delta.IsAdded() { + log.V(1).Info("Delta found", "create", desiredService.GetName()) + if err = r.client.Create(ctx, desiredService); err != nil { + return + } + } + if delta.IsUpdated() { + log.V(1).Info("Delta found", "update", existingService.GetName()) + rp := existingService.DeepCopy() + rp.Annotations = desiredService.Annotations + rp.Labels = desiredService.Labels + rp.Spec = desiredService.Spec + + if err = r.client.Update(ctx, rp); err != nil { + return + } + } + if delta.IsRemoved() { + log.V(1).Info("Delta found", "delete", existingService.GetName()) + if err = r.client.Delete(ctx, existingService); err != nil { + return + } + } + return nil +} + +func (r *KserveIstioServiceMonitorReconciler) DeleteServiceMonitor(ctx context.Context, isvcNamespace string) error { + return r.serviceMonitorHandler.DeleteServiceMonitor(ctx, types.NamespacedName{Name: istioServiceMonitorName, Namespace: isvcNamespace}) +} diff --git a/controllers/reconcilers/kserve_istio_smmr_reconciler.go b/controllers/reconcilers/kserve_istio_smmr_reconciler.go new file mode 100644 index 00000000..93316935 --- /dev/null +++ b/controllers/reconcilers/kserve_istio_smmr_reconciler.go @@ -0,0 +1,147 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package reconcilers + +import ( + "context" + "github.com/go-logr/logr" + kservev1beta1 "github.com/kserve/kserve/pkg/apis/serving/v1beta1" + "github.com/opendatahub-io/odh-model-controller/controllers/comparators" + "github.com/opendatahub-io/odh-model-controller/controllers/constants" + "github.com/opendatahub-io/odh-model-controller/controllers/processors" + "github.com/opendatahub-io/odh-model-controller/controllers/resources" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + v1 "maistra.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + serviceMeshMemberRollName = "default" +) + +type KserveIstioSMMRReconciler struct { + client client.Client + scheme *runtime.Scheme + smmrHandler resources.ServiceMeshMemberRollHandler + deltaProcessor processors.DeltaProcessor +} + +func NewKServeIstioSMMRReconciler(client client.Client, scheme *runtime.Scheme) *KserveIstioSMMRReconciler { + return &KserveIstioSMMRReconciler{ + client: client, + scheme: scheme, + smmrHandler: resources.NewServiceMeshMemberRole(client), + deltaProcessor: processors.NewDeltaProcessor(), + } +} + +func (r *KserveIstioSMMRReconciler) Reconcile(ctx context.Context, log logr.Logger, isvc *kservev1beta1.InferenceService) error { + + // Create Desired resource + desiredResource, err := r.createDesiredResource(ctx, log, isvc) + if err != nil { + return err + } + + // Get Existing resource + existingResource, err := r.getExistingResource(ctx, log) + if err != nil { + return err + } + + // Process Delta + if err = r.processDelta(ctx, log, desiredResource, existingResource); err != nil { + return err + } + return nil +} + +func (r *KserveIstioSMMRReconciler) createDesiredResource(ctx context.Context, log logr.Logger, isvc *kservev1beta1.InferenceService) (*v1.ServiceMeshMemberRoll, error) { + desiredSMMR, err := r.smmrHandler.FetchSMMR(ctx, log, types.NamespacedName{Name: serviceMeshMemberRollName, Namespace: constants.IstioNamespace}) + if err != nil { + return nil, err + } + + if desiredSMMR != nil { + //check if the namespace is already in the list, if it does not exists, append and update + serviceMeshMemberRollEntryExists := false + memberList := desiredSMMR.Spec.Members + for _, member := range memberList { + if member == isvc.Namespace { + serviceMeshMemberRollEntryExists = true + } + } + if !serviceMeshMemberRollEntryExists { + desiredSMMR.Spec.Members = append(memberList, isvc.Namespace) + } + } else { + desiredSMMR = &v1.ServiceMeshMemberRoll{ + ObjectMeta: metav1.ObjectMeta{ + Name: serviceMeshMemberRollName, + Namespace: constants.IstioNamespace, + }, + Spec: v1.ServiceMeshMemberRollSpec{ + Members: []string{ + isvc.Namespace, + }, + }, + } + } + return desiredSMMR, nil +} + +func (r *KserveIstioSMMRReconciler) getExistingResource(ctx context.Context, log logr.Logger) (*v1.ServiceMeshMemberRoll, error) { + return r.smmrHandler.FetchSMMR(ctx, log, types.NamespacedName{Name: serviceMeshMemberRollName, Namespace: constants.IstioNamespace}) +} + +func (r *KserveIstioSMMRReconciler) processDelta(ctx context.Context, log logr.Logger, desiredSMMR *v1.ServiceMeshMemberRoll, existingSMMR *v1.ServiceMeshMemberRoll) (err error) { + comparator := comparators.GetServiceMeshMemberRollComparator() + delta := r.deltaProcessor.ComputeDelta(comparator, desiredSMMR, existingSMMR) + + if !delta.HasChanges() { + log.V(1).Info("No delta found") + return nil + } + + if delta.IsAdded() { + log.V(1).Info("Delta found", "create", desiredSMMR.GetName()) + if err = r.client.Create(ctx, desiredSMMR); err != nil { + return + } + } + if delta.IsUpdated() { + log.V(1).Info("Delta found", "update", existingSMMR.GetName()) + rp := existingSMMR.DeepCopy() + rp.Spec = desiredSMMR.Spec + + if err = r.client.Update(ctx, rp); err != nil { + return + } + } + if delta.IsRemoved() { + log.V(1).Info("Delta found", "delete", existingSMMR.GetName()) + if err = r.client.Delete(ctx, existingSMMR); err != nil { + return + } + } + return nil +} + +func (r *KserveIstioSMMRReconciler) RemoveMemberFromSMMR(ctx context.Context, isvcNamespace string) error { + return r.smmrHandler.RemoveMemberFromSMMR(ctx, types.NamespacedName{Name: serviceMeshMemberRollName, Namespace: constants.IstioNamespace}, isvcNamespace) +} diff --git a/controllers/reconcilers/kserve_istio_telemetry_reconciler.go b/controllers/reconcilers/kserve_istio_telemetry_reconciler.go new file mode 100644 index 00000000..6051efc9 --- /dev/null +++ b/controllers/reconcilers/kserve_istio_telemetry_reconciler.go @@ -0,0 +1,141 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package reconcilers + +import ( + "context" + "github.com/go-logr/logr" + kservev1beta1 "github.com/kserve/kserve/pkg/apis/serving/v1beta1" + "github.com/opendatahub-io/odh-model-controller/controllers/comparators" + "github.com/opendatahub-io/odh-model-controller/controllers/processors" + "github.com/opendatahub-io/odh-model-controller/controllers/resources" + "istio.io/api/telemetry/v1alpha1" + istiotypes "istio.io/api/type/v1beta1" + telemetryv1alpha1 "istio.io/client-go/pkg/apis/telemetry/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + telemetryName = "enable-prometheus-metrics" +) + +type KserveIstioTelemetryReconciler struct { + client client.Client + scheme *runtime.Scheme + telemetryHandler resources.TelemetryHandler + deltaProcessor processors.DeltaProcessor +} + +func NewKServeIstioTelemetryReconciler(client client.Client, scheme *runtime.Scheme) *KserveIstioTelemetryReconciler { + return &KserveIstioTelemetryReconciler{ + client: client, + scheme: scheme, + telemetryHandler: resources.NewTelemetryHandler(client), + deltaProcessor: processors.NewDeltaProcessor(), + } +} + +func (r *KserveIstioTelemetryReconciler) Reconcile(ctx context.Context, log logr.Logger, isvc *kservev1beta1.InferenceService) error { + + // Create Desired resource + desiredResource, err := r.createDesiredResource(isvc) + if err != nil { + return err + } + + // Get Existing resource + existingResource, err := r.getExistingResource(ctx, log, isvc) + if err != nil { + return err + } + + // Process Delta + if err = r.processDelta(ctx, log, desiredResource, existingResource); err != nil { + return err + } + return nil +} + +func (r *KserveIstioTelemetryReconciler) createDesiredResource(isvc *kservev1beta1.InferenceService) (*telemetryv1alpha1.Telemetry, error) { + desiredTelemetry := &telemetryv1alpha1.Telemetry{ + ObjectMeta: metav1.ObjectMeta{ + Name: telemetryName, + Namespace: isvc.Namespace, + }, + Spec: v1alpha1.Telemetry{ + Selector: &istiotypes.WorkloadSelector{ + MatchLabels: map[string]string{ + "component": "predictor", + }, + }, + Metrics: []*v1alpha1.Metrics{ + { + Providers: []*v1alpha1.ProviderRef{ + { + Name: "prometheus", + }, + }, + }, + }, + }, + } + return desiredTelemetry, nil +} + +func (r *KserveIstioTelemetryReconciler) getExistingResource(ctx context.Context, log logr.Logger, isvc *kservev1beta1.InferenceService) (*telemetryv1alpha1.Telemetry, error) { + return r.telemetryHandler.FetchTelemetry(ctx, log, types.NamespacedName{Name: telemetryName, Namespace: isvc.Namespace}) +} + +func (r *KserveIstioTelemetryReconciler) processDelta(ctx context.Context, log logr.Logger, desiredTelemetry *telemetryv1alpha1.Telemetry, existingTelemetry *telemetryv1alpha1.Telemetry) (err error) { + comparator := comparators.GetTelemetryComparator() + delta := r.deltaProcessor.ComputeDelta(comparator, desiredTelemetry, existingTelemetry) + + if !delta.HasChanges() { + log.V(1).Info("No delta found") + return nil + } + + if delta.IsAdded() { + log.V(1).Info("Delta found", "create", desiredTelemetry.GetName()) + if err = r.client.Create(ctx, desiredTelemetry); err != nil { + return + } + } + if delta.IsUpdated() { + log.V(1).Info("Delta found", "update", existingTelemetry.GetName()) + rp := existingTelemetry.DeepCopy() + rp.Spec.Selector = desiredTelemetry.Spec.Selector + rp.Spec.Metrics = desiredTelemetry.Spec.Metrics + + if err = r.client.Update(ctx, rp); err != nil { + return + } + } + if delta.IsRemoved() { + log.V(1).Info("Delta found", "delete", existingTelemetry.GetName()) + if err = r.client.Delete(ctx, existingTelemetry); err != nil { + return + } + } + return nil +} + +func (r *KserveIstioTelemetryReconciler) DeleteTelemetry(ctx context.Context, isvcNamespace string) error { + return r.telemetryHandler.DeleteTelemetry(ctx, types.NamespacedName{Name: telemetryName, Namespace: isvcNamespace}) +} diff --git a/controllers/reconcilers/kserve_metrics_service_reconciler.go b/controllers/reconcilers/kserve_metrics_service_reconciler.go new file mode 100644 index 00000000..40bb1e3d --- /dev/null +++ b/controllers/reconcilers/kserve_metrics_service_reconciler.go @@ -0,0 +1,153 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package reconcilers + +import ( + "context" + "github.com/go-logr/logr" + kservev1beta1 "github.com/kserve/kserve/pkg/apis/serving/v1beta1" + "github.com/opendatahub-io/odh-model-controller/controllers/comparators" + "github.com/opendatahub-io/odh-model-controller/controllers/processors" + "github.com/opendatahub-io/odh-model-controller/controllers/resources" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + inferenceServiceLabelName = "serving.kserve.io/inferenceservice" +) + +type KserveMetricsServiceReconciler struct { + client client.Client + scheme *runtime.Scheme + serviceHandler resources.ServiceHandler + deltaProcessor processors.DeltaProcessor +} + +func NewKServeMetricsServiceReconciler(client client.Client, scheme *runtime.Scheme) *KserveMetricsServiceReconciler { + return &KserveMetricsServiceReconciler{ + client: client, + scheme: scheme, + serviceHandler: resources.NewServiceHandler(client), + deltaProcessor: processors.NewDeltaProcessor(), + } +} + +func (r *KserveMetricsServiceReconciler) Reconcile(ctx context.Context, log logr.Logger, isvc *kservev1beta1.InferenceService) error { + + // Create Desired resource + desiredResource, err := r.createDesiredResource(log, isvc) + if err != nil { + return err + } + + // Get Existing resource + existingResource, err := r.getExistingResource(ctx, log, isvc) + if err != nil { + return err + } + + // Process Delta + if err = r.processDelta(ctx, log, desiredResource, existingResource); err != nil { + return err + } + return nil +} + +func (r *KserveMetricsServiceReconciler) createDesiredResource(log logr.Logger, isvc *kservev1beta1.InferenceService) (*v1.Service, error) { + metricsService := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: getMetricsServiceName(isvc), + Namespace: isvc.Namespace, + Labels: map[string]string{ + "name": getMetricsServiceName(isvc), + }, + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + { + Name: "caikit-metrics", + Protocol: v1.ProtocolTCP, + Port: 8086, + TargetPort: intstr.FromInt(8086), + }, + { + Name: "tgis-metrics", + Protocol: v1.ProtocolTCP, + Port: 3000, + TargetPort: intstr.FromInt(3000), + }, + }, + Type: v1.ServiceTypeClusterIP, + Selector: map[string]string{ + inferenceServiceLabelName: isvc.Name, + }, + }, + } + if err := ctrl.SetControllerReference(isvc, metricsService, r.scheme); err != nil { + log.Error(err, "Unable to add OwnerReference to the Metrics Service") + return nil, err + } + return metricsService, nil +} + +func (r *KserveMetricsServiceReconciler) getExistingResource(ctx context.Context, log logr.Logger, isvc *kservev1beta1.InferenceService) (*v1.Service, error) { + return r.serviceHandler.FetchService(ctx, log, types.NamespacedName{Name: getMetricsServiceName(isvc), Namespace: isvc.Namespace}) +} + +func (r *KserveMetricsServiceReconciler) processDelta(ctx context.Context, log logr.Logger, desiredService *v1.Service, existingService *v1.Service) (err error) { + comparator := comparators.GetServiceComparator() + delta := r.deltaProcessor.ComputeDelta(comparator, desiredService, existingService) + + if !delta.HasChanges() { + log.V(1).Info("No delta found") + return nil + } + + if delta.IsAdded() { + log.V(1).Info("Delta found", "create", desiredService.GetName()) + if err = r.client.Create(ctx, desiredService); err != nil { + return + } + } + if delta.IsUpdated() { + log.V(1).Info("Delta found", "update", existingService.GetName()) + rp := existingService.DeepCopy() + rp.Annotations = desiredService.Annotations + rp.Labels = desiredService.Labels + rp.Spec = desiredService.Spec + + if err = r.client.Update(ctx, rp); err != nil { + return + } + } + if delta.IsRemoved() { + log.V(1).Info("Delta found", "delete", existingService.GetName()) + if err = r.client.Delete(ctx, existingService); err != nil { + return + } + } + return nil +} + +func getMetricsServiceName(isvc *kservev1beta1.InferenceService) string { + return isvc.Name + "-metrics" +} diff --git a/controllers/reconcilers/kserve_metrics_servicemonitor_reconciler.go b/controllers/reconcilers/kserve_metrics_servicemonitor_reconciler.go new file mode 100644 index 00000000..7dac357b --- /dev/null +++ b/controllers/reconcilers/kserve_metrics_servicemonitor_reconciler.go @@ -0,0 +1,140 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package reconcilers + +import ( + "context" + "github.com/go-logr/logr" + kservev1beta1 "github.com/kserve/kserve/pkg/apis/serving/v1beta1" + "github.com/opendatahub-io/odh-model-controller/controllers/comparators" + "github.com/opendatahub-io/odh-model-controller/controllers/processors" + "github.com/opendatahub-io/odh-model-controller/controllers/resources" + v1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type KserveMetricsServiceMonitorReconciler struct { + client client.Client + scheme *runtime.Scheme + serviceMonitorHandler resources.ServiceMonitorHandler + deltaProcessor processors.DeltaProcessor +} + +func NewKServeMetricsServiceMonitorReconciler(client client.Client, scheme *runtime.Scheme) *KserveMetricsServiceMonitorReconciler { + return &KserveMetricsServiceMonitorReconciler{ + client: client, + scheme: scheme, + serviceMonitorHandler: resources.NewServiceMonitorHandler(client), + deltaProcessor: processors.NewDeltaProcessor(), + } +} + +func (r *KserveMetricsServiceMonitorReconciler) Reconcile(ctx context.Context, log logr.Logger, isvc *kservev1beta1.InferenceService) error { + + // Create Desired resource + desiredResource, err := r.createDesiredResource(isvc) + if err != nil { + return err + } + + // Get Existing resource + existingResource, err := r.getExistingResource(ctx, log, isvc) + if err != nil { + return err + } + + // Process Delta + if err = r.processDelta(ctx, log, desiredResource, existingResource); err != nil { + return err + } + return nil +} + +func (r *KserveMetricsServiceMonitorReconciler) createDesiredResource(isvc *kservev1beta1.InferenceService) (*v1.ServiceMonitor, error) { + desiredServiceMonitor := &v1.ServiceMonitor{ + ObjectMeta: metav1.ObjectMeta{ + Name: getMetricsServiceMonitorName(isvc), + Namespace: isvc.Namespace, + }, + Spec: v1.ServiceMonitorSpec{ + Endpoints: []v1.Endpoint{ + { + Port: "caikit-metrics", + Scheme: "http", + }, + { + Port: "tgis-metrics", + Scheme: "http", + }, + }, + NamespaceSelector: v1.NamespaceSelector{}, + Selector: metav1.LabelSelector{ + MatchLabels: map[string]string{ + "name": getMetricsServiceMonitorName(isvc), + }, + }, + }, + } + if err := ctrl.SetControllerReference(isvc, desiredServiceMonitor, r.scheme); err != nil { + return nil, err + } + return desiredServiceMonitor, nil +} + +func (r *KserveMetricsServiceMonitorReconciler) getExistingResource(ctx context.Context, log logr.Logger, isvc *kservev1beta1.InferenceService) (*v1.ServiceMonitor, error) { + return r.serviceMonitorHandler.FetchServiceMonitor(ctx, log, types.NamespacedName{Name: getMetricsServiceMonitorName(isvc), Namespace: isvc.Namespace}) +} + +func (r *KserveMetricsServiceMonitorReconciler) processDelta(ctx context.Context, log logr.Logger, desiredService *v1.ServiceMonitor, existingService *v1.ServiceMonitor) (err error) { + comparator := comparators.GetServiceMonitorComparator() + delta := r.deltaProcessor.ComputeDelta(comparator, desiredService, existingService) + + if !delta.HasChanges() { + log.V(1).Info("No delta found") + return nil + } + + if delta.IsAdded() { + log.V(1).Info("Delta found", "create", desiredService.GetName()) + if err = r.client.Create(ctx, desiredService); err != nil { + return + } + } + if delta.IsUpdated() { + log.V(1).Info("Delta found", "update", existingService.GetName()) + rp := existingService.DeepCopy() + rp.Spec = desiredService.Spec + + if err = r.client.Update(ctx, rp); err != nil { + return + } + } + if delta.IsRemoved() { + log.V(1).Info("Delta found", "delete", existingService.GetName()) + if err = r.client.Delete(ctx, existingService); err != nil { + return + } + } + return nil +} + +func getMetricsServiceMonitorName(isvc *kservev1beta1.InferenceService) string { + return isvc.Name + "-metrics" +} diff --git a/controllers/reconcilers/kserve_networkpolicy_reconciler.go b/controllers/reconcilers/kserve_networkpolicy_reconciler.go new file mode 100644 index 00000000..8ac45232 --- /dev/null +++ b/controllers/reconcilers/kserve_networkpolicy_reconciler.go @@ -0,0 +1,144 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package reconcilers + +import ( + "context" + "github.com/go-logr/logr" + kservev1beta1 "github.com/kserve/kserve/pkg/apis/serving/v1beta1" + "github.com/opendatahub-io/odh-model-controller/controllers/comparators" + "github.com/opendatahub-io/odh-model-controller/controllers/processors" + "github.com/opendatahub-io/odh-model-controller/controllers/resources" + "k8s.io/api/networking/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + networkPolicyName = "allow-from-openshift-monitoring-ns" +) + +type KserveNetworkPolicyReconciler struct { + client client.Client + scheme *runtime.Scheme + networkPolicyHandler resources.NetworkPolicyHandler + deltaProcessor processors.DeltaProcessor +} + +func NewKServeNetworkPolicyReconciler(client client.Client, scheme *runtime.Scheme) *KserveNetworkPolicyReconciler { + return &KserveNetworkPolicyReconciler{ + client: client, + scheme: scheme, + networkPolicyHandler: resources.NewNetworkPolicyHandler(client), + deltaProcessor: processors.NewDeltaProcessor(), + } +} + +func (r *KserveNetworkPolicyReconciler) Reconcile(ctx context.Context, log logr.Logger, isvc *kservev1beta1.InferenceService) error { + + // Create Desired resource + desiredResource, err := r.createDesiredResource(isvc) + if err != nil { + return err + } + + // Get Existing resource + existingResource, err := r.getExistingResource(ctx, log, isvc) + if err != nil { + return err + } + + // Process Delta + if err = r.processDelta(ctx, log, desiredResource, existingResource); err != nil { + return err + } + return nil +} + +func (r *KserveNetworkPolicyReconciler) createDesiredResource(isvc *kservev1beta1.InferenceService) (*v1.NetworkPolicy, error) { + desiredNetworkPolicy := &v1.NetworkPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: networkPolicyName, + Namespace: isvc.Namespace, + Labels: map[string]string{ + "app.kubernetes.io/version": "release-v1.9", + "networking.knative.dev/ingress-provider": "istio", + }, + }, + Spec: v1.NetworkPolicySpec{ + PodSelector: metav1.LabelSelector{}, + Ingress: []v1.NetworkPolicyIngressRule{ + { + From: []v1.NetworkPolicyPeer{ + { + NamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "name": "openshift-user-workload-monitoring", + }, + }, + }, + }, + }, + }, + PolicyTypes: []v1.PolicyType{v1.PolicyTypeIngress}, + }, + } + return desiredNetworkPolicy, nil +} + +func (r *KserveNetworkPolicyReconciler) getExistingResource(ctx context.Context, log logr.Logger, isvc *kservev1beta1.InferenceService) (*v1.NetworkPolicy, error) { + return r.networkPolicyHandler.FetchNetworkPolicy(ctx, log, types.NamespacedName{Name: networkPolicyName, Namespace: isvc.Namespace}) +} + +func (r *KserveNetworkPolicyReconciler) processDelta(ctx context.Context, log logr.Logger, desiredPod *v1.NetworkPolicy, existingPod *v1.NetworkPolicy) (err error) { + comparator := comparators.GetNetworkPolicyComparator() + delta := r.deltaProcessor.ComputeDelta(comparator, desiredPod, existingPod) + + if !delta.HasChanges() { + log.V(1).Info("No delta found") + return nil + } + + if delta.IsAdded() { + log.V(1).Info("Delta found", "create", desiredPod.GetName()) + if err = r.client.Create(ctx, desiredPod); err != nil { + return + } + } + if delta.IsUpdated() { + log.V(1).Info("Delta found", "update", existingPod.GetName()) + rp := existingPod.DeepCopy() + rp.Labels = desiredPod.Labels + rp.Spec = desiredPod.Spec + + if err = r.client.Update(ctx, rp); err != nil { + return + } + } + if delta.IsRemoved() { + log.V(1).Info("Delta found", "delete", existingPod.GetName()) + if err = r.client.Delete(ctx, existingPod); err != nil { + return + } + } + return nil +} + +func (r *KserveNetworkPolicyReconciler) DeleteNetworkPolicy(ctx context.Context, isvcNamespace string) error { + return r.networkPolicyHandler.DeleteNetworkPolicy(ctx, types.NamespacedName{Name: networkPolicyName, Namespace: isvcNamespace}) +} diff --git a/controllers/reconcilers/kserve_prometheus_rolebinding_reconciler.go b/controllers/reconcilers/kserve_prometheus_rolebinding_reconciler.go new file mode 100644 index 00000000..8c01889f --- /dev/null +++ b/controllers/reconcilers/kserve_prometheus_rolebinding_reconciler.go @@ -0,0 +1,135 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package reconcilers + +import ( + "context" + "github.com/go-logr/logr" + kservev1beta1 "github.com/kserve/kserve/pkg/apis/serving/v1beta1" + "github.com/opendatahub-io/odh-model-controller/controllers/comparators" + "github.com/opendatahub-io/odh-model-controller/controllers/processors" + "github.com/opendatahub-io/odh-model-controller/controllers/resources" + v1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + clusterPrometheusAccessRoleBinding = "kserve-prometheus-k8s" +) + +type KservePrometheusRoleBindingReconciler struct { + client client.Client + scheme *runtime.Scheme + roleBindingHandler resources.RoleBindingHandler + deltaProcessor processors.DeltaProcessor +} + +func NewKServePrometheusRoleBindingReconciler(client client.Client, scheme *runtime.Scheme) *KservePrometheusRoleBindingReconciler { + return &KservePrometheusRoleBindingReconciler{ + client: client, + scheme: scheme, + roleBindingHandler: resources.NewRoleBindingHandler(client), + deltaProcessor: processors.NewDeltaProcessor(), + } +} + +func (r *KservePrometheusRoleBindingReconciler) Reconcile(ctx context.Context, log logr.Logger, isvc *kservev1beta1.InferenceService) error { + + // Create Desired resource + desiredResource, err := r.createDesiredResource(isvc) + if err != nil { + return err + } + + // Get Existing resource + existingResource, err := r.getExistingResource(ctx, log, isvc) + if err != nil { + return err + } + + // Process Delta + if err = r.processDelta(ctx, log, desiredResource, existingResource); err != nil { + return err + } + return nil +} + +func (r *KservePrometheusRoleBindingReconciler) createDesiredResource(isvc *kservev1beta1.InferenceService) (*v1.RoleBinding, error) { + desiredRoleBinding := &v1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterPrometheusAccessRoleBinding, + Namespace: isvc.Namespace, + }, + RoleRef: v1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "ClusterRole", + Name: "kserve-prometheus-k8s", + }, + Subjects: []v1.Subject{ + { + Kind: "ServiceAccount", + Name: "prometheus-k8s", + Namespace: "openshift-monitoring", + }, + }, + } + return desiredRoleBinding, nil +} + +func (r *KservePrometheusRoleBindingReconciler) getExistingResource(ctx context.Context, log logr.Logger, isvc *kservev1beta1.InferenceService) (*v1.RoleBinding, error) { + return r.roleBindingHandler.FetchRoleBinding(ctx, log, types.NamespacedName{Name: clusterPrometheusAccessRoleBinding, Namespace: isvc.Namespace}) +} + +func (r *KservePrometheusRoleBindingReconciler) processDelta(ctx context.Context, log logr.Logger, desiredRoleBinding *v1.RoleBinding, existingRoleBinding *v1.RoleBinding) (err error) { + comparator := comparators.GetRoleBindingComparator() + delta := r.deltaProcessor.ComputeDelta(comparator, desiredRoleBinding, existingRoleBinding) + + if !delta.HasChanges() { + log.V(1).Info("No delta found") + return nil + } + + if delta.IsAdded() { + log.V(1).Info("Delta found", "create", desiredRoleBinding.GetName()) + if err = r.client.Create(ctx, desiredRoleBinding); err != nil { + return + } + } + if delta.IsUpdated() { + log.V(1).Info("Delta found", "update", existingRoleBinding.GetName()) + rp := existingRoleBinding.DeepCopy() + rp.RoleRef = desiredRoleBinding.RoleRef + rp.Subjects = desiredRoleBinding.Subjects + + if err = r.client.Update(ctx, rp); err != nil { + return + } + } + if delta.IsRemoved() { + log.V(1).Info("Delta found", "delete", existingRoleBinding.GetName()) + if err = r.client.Delete(ctx, existingRoleBinding); err != nil { + return + } + } + return nil +} + +func (r *KservePrometheusRoleBindingReconciler) DeleteRoleBinding(ctx context.Context, isvcNamespace string) error { + return r.roleBindingHandler.DeleteRoleBinding(ctx, types.NamespacedName{Name: clusterPrometheusAccessRoleBinding, Namespace: isvcNamespace}) +} diff --git a/controllers/reconcilers/kisvc_route_reconciler.go b/controllers/reconcilers/kserve_route_reconciler.go similarity index 55% rename from controllers/reconcilers/kisvc_route_reconciler.go rename to controllers/reconcilers/kserve_route_reconciler.go index 573d2f7a..22c3f947 100644 --- a/controllers/reconcilers/kisvc_route_reconciler.go +++ b/controllers/reconcilers/kserve_route_reconciler.go @@ -1,3 +1,18 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package reconcilers import ( @@ -7,9 +22,10 @@ import ( kservev1beta1 "github.com/kserve/kserve/pkg/apis/serving/v1beta1" "github.com/kserve/kserve/pkg/constants" "github.com/kserve/kserve/pkg/utils" - "github.com/opendatahub-io/odh-model-controller/controllers/components" + "github.com/opendatahub-io/odh-model-controller/controllers/comparators" constants2 "github.com/opendatahub-io/odh-model-controller/controllers/constants" "github.com/opendatahub-io/odh-model-controller/controllers/processors" + "github.com/opendatahub-io/odh-model-controller/controllers/resources" v1 "github.com/openshift/api/route/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -20,51 +36,44 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -type kserveInferenceServiceRouteReconciler struct { +type KserveRouteReconciler struct { client client.Client scheme *runtime.Scheme - ctx context.Context - isvc *kservev1beta1.InferenceService - log logr.Logger - routeHandler components.RouteHandler + routeHandler resources.RouteHandler deltaProcessor processors.DeltaProcessor } -func NewKserveInferenceServiceRouteReconciler(client client.Client, scheme *runtime.Scheme, ctx context.Context, log logr.Logger, isvc *kservev1beta1.InferenceService) Reconciler { - logger := log.WithValues("resource", "KserveInferenceServiceRoute") - return &kserveInferenceServiceRouteReconciler{ +func NewKserveRouteReconciler(client client.Client, scheme *runtime.Scheme) *KserveRouteReconciler { + return &KserveRouteReconciler{ client: client, scheme: scheme, - ctx: ctx, - isvc: isvc, - log: logger, - routeHandler: components.NewRouteHandler(client, ctx, logger), + routeHandler: resources.NewRouteHandler(client), deltaProcessor: processors.NewDeltaProcessor(), } } -func (r *kserveInferenceServiceRouteReconciler) Reconcile() error { +func (r *KserveRouteReconciler) Reconcile(ctx context.Context, log logr.Logger, isvc *kservev1beta1.InferenceService) error { // Create Desired resource - desiredResource, err := r.createDesiredResource() + desiredResource, err := r.createDesiredResource(isvc) if err != nil { return err } // Get Existing resource - existingResource, err := r.getExistingResource() + existingResource, err := r.getExistingResource(ctx, log, isvc) if err != nil { return err } // Process Delta - if err = r.processDelta(desiredResource, existingResource); err != nil { + if err = r.processDelta(ctx, log, desiredResource, existingResource); err != nil { return err } return nil } -func (r *kserveInferenceServiceRouteReconciler) createDesiredResource() (*v1.Route, error) { +func (r *KserveRouteReconciler) createDesiredResource(isvc *kservev1beta1.InferenceService) (*v1.Route, error) { ingressConfig, err := kservev1beta1.NewIngressConfig(r.client) if err != nil { return nil, err @@ -73,16 +82,16 @@ func (r *kserveInferenceServiceRouteReconciler) createDesiredResource() (*v1.Rou disableIstioVirtualHost := ingressConfig.DisableIstioVirtualHost if disableIstioVirtualHost == false { - serviceHost := getServiceHost(r.isvc) + serviceHost := getServiceHost(isvc) if serviceHost == "" { return nil, fmt.Errorf("failed to load serviceHost from InferenceService status") } isInternal := false //if service is labelled with cluster local or knative domain is configured as internal - if val, ok := r.isvc.Labels[constants.VisibilityLabel]; ok && val == constants.ClusterLocalVisibility { + if val, ok := isvc.Labels[constants.VisibilityLabel]; ok && val == constants.ClusterLocalVisibility { isInternal = true } - serviceInternalHostName := network.GetServiceHostname(r.isvc.Name, r.isvc.Namespace) + serviceInternalHostName := network.GetServiceHostname(isvc.Name, isvc.Namespace) if serviceHost == serviceInternalHostName { isInternal = true } @@ -93,7 +102,7 @@ func (r *kserveInferenceServiceRouteReconciler) createDesiredResource() (*v1.Rou if ingressConfig.PathTemplate != "" { serviceHost = ingressConfig.IngressDomain } - annotations := utils.Filter(r.isvc.Annotations, func(key string) bool { + annotations := utils.Filter(isvc.Annotations, func(key string) bool { return !utils.Includes(constants.ServiceAnnotationDisallowedList, key) }) @@ -112,10 +121,10 @@ func (r *kserveInferenceServiceRouteReconciler) createDesiredResource() (*v1.Rou route := &v1.Route{ ObjectMeta: metav1.ObjectMeta{ - Name: GetKServeRouteName(r.isvc), + Name: getKServeRouteName(isvc), Namespace: constants2.IstioNamespace, Annotations: annotations, - Labels: r.isvc.Labels, + Labels: isvc.Labels, }, Spec: v1.RouteSpec{ Host: serviceHost, @@ -139,39 +148,39 @@ func (r *kserveInferenceServiceRouteReconciler) createDesiredResource() (*v1.Rou return nil, nil } -func (r *kserveInferenceServiceRouteReconciler) getExistingResource() (*v1.Route, error) { - return r.routeHandler.FetchRoute(types.NamespacedName{Name: GetKServeRouteName(r.isvc), Namespace: constants2.IstioNamespace}) +func (r *KserveRouteReconciler) getExistingResource(ctx context.Context, log logr.Logger, isvc *kservev1beta1.InferenceService) (*v1.Route, error) { + return r.routeHandler.FetchRoute(ctx, log, types.NamespacedName{Name: getKServeRouteName(isvc), Namespace: constants2.IstioNamespace}) } -func (r *kserveInferenceServiceRouteReconciler) processDelta(desiredRoute *v1.Route, existingRoute *v1.Route) (err error) { - comparator := r.routeHandler.GetComparator() +func (r *KserveRouteReconciler) processDelta(ctx context.Context, log logr.Logger, desiredRoute *v1.Route, existingRoute *v1.Route) (err error) { + comparator := comparators.GetKServeRouteComparator() delta := r.deltaProcessor.ComputeDelta(comparator, desiredRoute, existingRoute) if !delta.HasChanges() { - r.log.Info("No delta found") + log.V(1).Info("No delta found") return nil } if delta.IsAdded() { - r.log.Info("Will", "create", desiredRoute.GetName()) - if err = r.client.Create(r.ctx, desiredRoute); err != nil { + log.V(1).Info("Delta found", "create", desiredRoute.GetName()) + if err = r.client.Create(ctx, desiredRoute); err != nil { return } } if delta.IsUpdated() { - r.log.Info("Will", "update", existingRoute.GetName()) + log.V(1).Info("Delta found", "update", existingRoute.GetName()) rp := existingRoute.DeepCopy() rp.Labels = desiredRoute.Labels rp.Annotations = desiredRoute.Annotations rp.Spec = desiredRoute.Spec - if err = r.client.Update(r.ctx, rp); err != nil { + if err = r.client.Update(ctx, rp); err != nil { return } } if delta.IsRemoved() { - r.log.Info("Will", "delete", existingRoute.GetName()) - if err = r.client.Delete(r.ctx, existingRoute); err != nil { + log.V(1).Info("Delta found", "delete", existingRoute.GetName()) + if err = r.client.Delete(ctx, existingRoute); err != nil { return } } @@ -186,6 +195,10 @@ func getServiceHost(isvc *kservev1beta1.InferenceService) string { return isvc.Status.URL.Host } -func GetKServeRouteName(isvc *kservev1beta1.InferenceService) string { +func getKServeRouteName(isvc *kservev1beta1.InferenceService) string { return isvc.Name + "-" + isvc.Namespace } + +func (r *KserveRouteReconciler) DeleteRoute(ctx context.Context, isvc *kservev1beta1.InferenceService) error { + return r.routeHandler.DeleteRoute(ctx, types.NamespacedName{Name: getKServeRouteName(isvc), Namespace: constants2.IstioNamespace}) +} diff --git a/controllers/reconcilers/mm_clusterrolebinding_reconciler.go b/controllers/reconcilers/mm_clusterrolebinding_reconciler.go new file mode 100644 index 00000000..e2e35937 --- /dev/null +++ b/controllers/reconcilers/mm_clusterrolebinding_reconciler.go @@ -0,0 +1,134 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package reconcilers + +import ( + "context" + "github.com/go-logr/logr" + kservev1beta1 "github.com/kserve/kserve/pkg/apis/serving/v1beta1" + "github.com/opendatahub-io/odh-model-controller/controllers/comparators" + "github.com/opendatahub-io/odh-model-controller/controllers/processors" + "github.com/opendatahub-io/odh-model-controller/controllers/resources" + v1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type ModelMeshClusterRoleBindingReconciler struct { + client client.Client + scheme *runtime.Scheme + clusterRoleBindingHandler resources.ClusterRoleBindingHandler + deltaProcessor processors.DeltaProcessor +} + +func NewModelMeshClusterRoleBindingReconciler(client client.Client, scheme *runtime.Scheme) *ModelMeshClusterRoleBindingReconciler { + return &ModelMeshClusterRoleBindingReconciler{ + client: client, + scheme: scheme, + clusterRoleBindingHandler: resources.NewClusterRoleBindingHandler(client), + deltaProcessor: processors.NewDeltaProcessor(), + } +} + +func (r *ModelMeshClusterRoleBindingReconciler) Reconcile(ctx context.Context, log logr.Logger, isvc *kservev1beta1.InferenceService) error { + // Create Desired resource + desiredResource, err := r.createDesiredResource(isvc) + if err != nil { + return err + } + + // Get Existing resource + existingResource, err := r.getExistingResource(ctx, log, isvc) + if err != nil { + return err + } + + // Process Delta + if err = r.processDelta(ctx, log, desiredResource, existingResource); err != nil { + return err + } + return nil +} + +func (r *ModelMeshClusterRoleBindingReconciler) createDesiredResource(isvc *kservev1beta1.InferenceService) (*v1.ClusterRoleBinding, error) { + desiredClusterRoleBinding := &v1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: getClusterRoleBindingName(isvc.Namespace), + Namespace: isvc.Namespace, + }, + Subjects: []v1.Subject{ + { + Kind: "ServiceAccount", + Namespace: isvc.Namespace, + Name: modelMeshServiceAccountName, + }, + }, + RoleRef: v1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "ClusterRole", + Name: "system:auth-delegator", + }, + } + return desiredClusterRoleBinding, nil +} + +func (r *ModelMeshClusterRoleBindingReconciler) getExistingResource(ctx context.Context, log logr.Logger, isvc *kservev1beta1.InferenceService) (*v1.ClusterRoleBinding, error) { + return r.clusterRoleBindingHandler.FetchClusterRoleBinding(ctx, log, types.NamespacedName{Name: getClusterRoleBindingName(isvc.Namespace), Namespace: isvc.Namespace}) +} + +func (r *ModelMeshClusterRoleBindingReconciler) processDelta(ctx context.Context, log logr.Logger, desiredCRB *v1.ClusterRoleBinding, existingCRB *v1.ClusterRoleBinding) (err error) { + comparator := comparators.GetClusterRoleBindingComparator() + delta := r.deltaProcessor.ComputeDelta(comparator, desiredCRB, existingCRB) + + if !delta.HasChanges() { + log.V(1).Info("No delta found") + return nil + } + + if delta.IsAdded() { + log.V(1).Info("Delta found", "create", desiredCRB.GetName()) + if err = r.client.Create(ctx, desiredCRB); err != nil { + return + } + } + if delta.IsUpdated() { + log.V(1).Info("Delta found", "update", existingCRB.GetName()) + rp := existingCRB.DeepCopy() + rp.RoleRef = desiredCRB.RoleRef + rp.Subjects = desiredCRB.Subjects + + if err = r.client.Update(ctx, rp); err != nil { + return + } + } + if delta.IsRemoved() { + log.V(1).Info("Delta found", "delete", existingCRB.GetName()) + if err = r.client.Delete(ctx, existingCRB); err != nil { + return + } + } + return nil +} + +func getClusterRoleBindingName(isvcNamespace string) string { + return isvcNamespace + "-" + modelMeshServiceAccountName + "-auth-delegator" +} + +func (r *ModelMeshClusterRoleBindingReconciler) DeleteClusterRoleBinding(ctx context.Context, isvcNamespace string) error { + return r.clusterRoleBindingHandler.DeleteClusterRoleBinding(ctx, types.NamespacedName{Name: getClusterRoleBindingName(isvcNamespace), Namespace: isvcNamespace}) +} diff --git a/controllers/reconcilers/mm_inferenceservice_reconciler.go b/controllers/reconcilers/mm_inferenceservice_reconciler.go new file mode 100644 index 00000000..e46a7de8 --- /dev/null +++ b/controllers/reconcilers/mm_inferenceservice_reconciler.go @@ -0,0 +1,89 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package reconcilers + +import ( + "context" + "github.com/go-logr/logr" + kservev1beta1 "github.com/kserve/kserve/pkg/apis/serving/v1beta1" + "github.com/opendatahub-io/odh-model-controller/controllers/utils" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type ModelMeshInferenceServiceReconciler struct { + client client.Client + routeReconciler *ModelMeshRouteReconciler + serviceAccountReconciler *ModelMeshServiceAccountReconciler + clusterRoleBindingReconciler *ModelMeshClusterRoleBindingReconciler +} + +func NewModelMeshInferenceServiceReconciler(client client.Client, scheme *runtime.Scheme) *ModelMeshInferenceServiceReconciler { + return &ModelMeshInferenceServiceReconciler{ + client: client, + routeReconciler: NewModelMeshRouteReconciler(client, scheme), + serviceAccountReconciler: NewModelMeshServiceAccountReconciler(client, scheme), + clusterRoleBindingReconciler: NewModelMeshClusterRoleBindingReconciler(client, scheme), + } +} + +func (r *ModelMeshInferenceServiceReconciler) Reconcile(ctx context.Context, log logr.Logger, isvc *kservev1beta1.InferenceService) error { + + log.V(1).Info("Reconciling Route for InferenceService") + if err := r.routeReconciler.Reconcile(ctx, log, isvc); err != nil { + return err + } + + log.V(1).Info("Reconciling ServiceAccount for InferenceService") + if err := r.serviceAccountReconciler.Reconcile(ctx, log, isvc); err != nil { + return err + } + + log.V(1).Info("Reconciling ClusterRoleBinding for InferenceService") + if err := r.clusterRoleBindingReconciler.Reconcile(ctx, log, isvc); err != nil { + return err + } + return nil +} + +func (r *ModelMeshInferenceServiceReconciler) DeleteModelMeshResourcesIfNoMMIsvcExists(ctx context.Context, log logr.Logger, isvcNamespace string) error { + inferenceServiceList := &kservev1beta1.InferenceServiceList{} + if err := r.client.List(ctx, inferenceServiceList, client.InNamespace(isvcNamespace)); err != nil { + return err + } + + for i := len(inferenceServiceList.Items) - 1; i >= 0; i-- { + inferenceService := inferenceServiceList.Items[i] + if !utils.IsDeploymentModeForIsvcModelMesh(&inferenceService) { + inferenceServiceList.Items = append(inferenceServiceList.Items[:i], inferenceServiceList.Items[i+1:]...) + } + } + + // If there are no ModelMesh InferenceServices in the namespace, delete namespace-scoped resources needed for ModelMesh + if len(inferenceServiceList.Items) == 0 { + + log.V(1).Info("Deleting ServiceAccount object for target namespace") + if err := r.serviceAccountReconciler.DeleteServiceAccount(ctx, isvcNamespace); err != nil { + return err + } + + log.V(1).Info("Deleting ClusterRoleBinding object for target namespace") + if err := r.clusterRoleBindingReconciler.DeleteClusterRoleBinding(ctx, isvcNamespace); err != nil { + return err + } + } + return nil +} diff --git a/controllers/reconcilers/mm_route_reconciler.go b/controllers/reconcilers/mm_route_reconciler.go new file mode 100644 index 00000000..f61b56a1 --- /dev/null +++ b/controllers/reconcilers/mm_route_reconciler.go @@ -0,0 +1,244 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package reconcilers + +import ( + "context" + "github.com/opendatahub-io/odh-model-controller/controllers/comparators" + "github.com/opendatahub-io/odh-model-controller/controllers/processors" + "github.com/opendatahub-io/odh-model-controller/controllers/resources" + "k8s.io/apimachinery/pkg/runtime" + "sort" + "strconv" + + "github.com/go-logr/logr" + kservev1alpha1 "github.com/kserve/kserve/pkg/apis/serving/v1alpha1" + kservev1beta1 "github.com/kserve/kserve/pkg/apis/serving/v1beta1" + "github.com/openshift/api/route/v1" + apierrs "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/pointer" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + modelmeshServiceName = "modelmesh-serving" + modelmeshAuthServicePort = 8443 + modelmeshServicePort = 8008 +) + +type ModelMeshRouteReconciler struct { + client client.Client + scheme *runtime.Scheme + routeHandler resources.RouteHandler + deltaProcessor processors.DeltaProcessor +} + +func NewModelMeshRouteReconciler(client client.Client, scheme *runtime.Scheme) *ModelMeshRouteReconciler { + return &ModelMeshRouteReconciler{ + client: client, + scheme: scheme, + routeHandler: resources.NewRouteHandler(client), + deltaProcessor: processors.NewDeltaProcessor(), + } +} + +// ReconcileRoute will manage the creation, update and deletion of the +// TLS route when the predictor is reconciled +func (r *ModelMeshRouteReconciler) Reconcile(ctx context.Context, log logr.Logger, isvc *kservev1beta1.InferenceService) error { + // Create Desired resource + desiredResource, err := r.createDesiredResource(ctx, log, isvc) + if err != nil { + return err + } + + // Get Existing resource + existingResource, err := r.getExistingResource(ctx, log, isvc) + if err != nil { + return err + } + + // Process Delta + if err = r.processDelta(ctx, log, desiredResource, existingResource); err != nil { + return err + } + return nil +} + +func (r *ModelMeshRouteReconciler) createDesiredResource(ctx context.Context, log logr.Logger, isvc *kservev1beta1.InferenceService) (*v1.Route, error) { + + desiredServingRuntime, err := r.findSupportingRuntimeForISvc(ctx, log, isvc) + if err != nil { + return nil, err + } + + enableAuth := false + if enableAuth, err = strconv.ParseBool(desiredServingRuntime.Annotations["enable-auth"]); err != nil { + enableAuth = false + } + createRoute := false + if createRoute, err = strconv.ParseBool(desiredServingRuntime.Annotations["enable-route"]); err != nil { + createRoute = false + } + + if !createRoute { + log.Info("Serving runtime does not have 'enable-route' annotation set to 'True'. Skipping route creation") + return nil, nil + } + + desiredRoute := &v1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Name: isvc.Name, + Namespace: isvc.Namespace, + Labels: map[string]string{ + "inferenceservice-name": isvc.Name, + }, + }, + Spec: v1.RouteSpec{ + To: v1.RouteTargetReference{ + Kind: "Service", + Name: modelmeshServiceName, + Weight: pointer.Int32(100), + }, + Port: &v1.RoutePort{ + TargetPort: intstr.FromInt(modelmeshServicePort), + }, + WildcardPolicy: v1.WildcardPolicyNone, + Path: "/v2/models/" + isvc.Name, + }, + Status: v1.RouteStatus{ + Ingress: []v1.RouteIngress{}, + }, + } + + if enableAuth { + desiredRoute.Spec.Port = &v1.RoutePort{ + TargetPort: intstr.FromInt(modelmeshAuthServicePort), + } + desiredRoute.Spec.TLS = &v1.TLSConfig{ + Termination: v1.TLSTerminationReencrypt, + InsecureEdgeTerminationPolicy: v1.InsecureEdgeTerminationPolicyRedirect, + } + } else { + desiredRoute.Spec.TLS = &v1.TLSConfig{ + Termination: v1.TLSTerminationEdge, + InsecureEdgeTerminationPolicy: v1.InsecureEdgeTerminationPolicyRedirect, + } + } + if err = ctrl.SetControllerReference(isvc, desiredRoute, r.scheme); err != nil { + return nil, err + } + return desiredRoute, nil +} + +func (r *ModelMeshRouteReconciler) getExistingResource(ctx context.Context, log logr.Logger, isvc *kservev1beta1.InferenceService) (*v1.Route, error) { + return r.routeHandler.FetchRoute(ctx, log, types.NamespacedName{Name: isvc.Name, Namespace: isvc.Namespace}) +} + +func (r *ModelMeshRouteReconciler) processDelta(ctx context.Context, log logr.Logger, desiredRoute *v1.Route, existingRoute *v1.Route) (err error) { + comparator := comparators.GetMMRouteComparator() + delta := r.deltaProcessor.ComputeDelta(comparator, desiredRoute, existingRoute) + + if !delta.HasChanges() { + log.V(1).Info("No delta found") + return nil + } + + if delta.IsAdded() { + log.V(1).Info("Delta found", "create", desiredRoute.GetName()) + if err = r.client.Create(ctx, desiredRoute); err != nil { + return + } + } + if delta.IsUpdated() { + log.V(1).Info("Delta found", "update", existingRoute.GetName()) + rp := existingRoute.DeepCopy() + rp.Labels = desiredRoute.Labels + rp.Annotations = desiredRoute.Annotations + rp.Spec = desiredRoute.Spec + + if err = r.client.Update(ctx, rp); err != nil { + return + } + } + if delta.IsRemoved() { + log.V(1).Info("Delta found", "delete", existingRoute.GetName()) + if err = r.client.Delete(ctx, existingRoute); err != nil { + return + } + } + return nil +} + +func (r *ModelMeshRouteReconciler) findSupportingRuntimeForISvc(ctx context.Context, log logr.Logger, isvc *kservev1beta1.InferenceService) (*kservev1alpha1.ServingRuntime, error) { + desiredServingRuntime := &kservev1alpha1.ServingRuntime{} + + if isvc.Spec.Predictor.Model.Runtime != nil { + err := r.client.Get(ctx, types.NamespacedName{ + Name: *isvc.Spec.Predictor.Model.Runtime, + Namespace: isvc.Namespace, + }, desiredServingRuntime) + if err != nil { + if apierrs.IsNotFound(err) { + return nil, err + } + } + return desiredServingRuntime, nil + } else { + runtimes := &kservev1alpha1.ServingRuntimeList{} + err := r.client.List(ctx, runtimes, client.InNamespace(isvc.Namespace)) + if err != nil { + return nil, err + } + + // Sort by creation date, to be somewhat deterministic + sort.Slice(runtimes.Items, func(i, j int) bool { + // Sorting descending by creation time leads to picking the most recently created runtimes first + if runtimes.Items[i].CreationTimestamp.Before(&runtimes.Items[j].CreationTimestamp) { + return false + } + if runtimes.Items[i].CreationTimestamp.Equal(&runtimes.Items[j].CreationTimestamp) { + // For Runtimes created at the same time, use alphabetical order. + return runtimes.Items[i].Name < runtimes.Items[j].Name + } + return true + }) + + for _, runtime := range runtimes.Items { + if runtime.Spec.Disabled != nil && *runtime.Spec.Disabled == true { + continue + } + + if runtime.Spec.MultiModel != nil && *runtime.Spec.MultiModel == false { + continue + } + + for _, supportedFormat := range runtime.Spec.SupportedModelFormats { + if supportedFormat.AutoSelect != nil && *supportedFormat.AutoSelect == true && supportedFormat.Name == isvc.Spec.Predictor.Model.ModelFormat.Name { + desiredServingRuntime = &runtime + log.Info("Automatic runtime selection for InferenceService", "runtime", desiredServingRuntime.Name) + return desiredServingRuntime, nil + } + } + } + + log.Info("No suitable Runtime available for InferenceService") + return desiredServingRuntime, nil + } +} diff --git a/controllers/reconcilers/mm_serviceaccount_reconciler.go b/controllers/reconcilers/mm_serviceaccount_reconciler.go new file mode 100644 index 00000000..618584e7 --- /dev/null +++ b/controllers/reconcilers/mm_serviceaccount_reconciler.go @@ -0,0 +1,121 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package reconcilers + +import ( + "context" + "github.com/go-logr/logr" + "github.com/opendatahub-io/odh-model-controller/controllers/comparators" + "github.com/opendatahub-io/odh-model-controller/controllers/processors" + "github.com/opendatahub-io/odh-model-controller/controllers/resources" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + "k8s.io/apimachinery/pkg/types" + + kservev1beta1 "github.com/kserve/kserve/pkg/apis/serving/v1beta1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + modelMeshServiceAccountName = "modelmesh-serving-sa" +) + +type ModelMeshServiceAccountReconciler struct { + client client.Client + scheme *runtime.Scheme + serviceAccountHandler resources.ServiceAccountHandler + deltaProcessor processors.DeltaProcessor +} + +func NewModelMeshServiceAccountReconciler(client client.Client, scheme *runtime.Scheme) *ModelMeshServiceAccountReconciler { + return &ModelMeshServiceAccountReconciler{ + client: client, + scheme: scheme, + serviceAccountHandler: resources.NewServiceAccountHandler(client), + deltaProcessor: processors.NewDeltaProcessor(), + } +} + +func (r *ModelMeshServiceAccountReconciler) Reconcile(ctx context.Context, log logr.Logger, isvc *kservev1beta1.InferenceService) error { + // Create Desired resource + desiredResource, err := r.createDesiredResource(isvc) + if err != nil { + return err + } + + // Get Existing resource + existingResource, err := r.getExistingResource(ctx, log, isvc) + if err != nil { + return err + } + + // Process Delta + if err = r.processDelta(ctx, log, desiredResource, existingResource); err != nil { + return err + } + return nil +} + +func (r *ModelMeshServiceAccountReconciler) createDesiredResource(isvc *kservev1beta1.InferenceService) (*corev1.ServiceAccount, error) { + desiredSA := &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: modelMeshServiceAccountName, + Namespace: isvc.Namespace, + }, + } + return desiredSA, nil +} + +func (r *ModelMeshServiceAccountReconciler) getExistingResource(ctx context.Context, log logr.Logger, isvc *kservev1beta1.InferenceService) (*corev1.ServiceAccount, error) { + return r.serviceAccountHandler.FetchServiceAccount(ctx, log, types.NamespacedName{Name: modelMeshServiceAccountName, Namespace: isvc.Namespace}) +} + +func (r *ModelMeshServiceAccountReconciler) processDelta(ctx context.Context, log logr.Logger, desiredSA *corev1.ServiceAccount, existingSA *corev1.ServiceAccount) (err error) { + comparator := comparators.GetServiceAccountComparator() + delta := r.deltaProcessor.ComputeDelta(comparator, desiredSA, existingSA) + + if !delta.HasChanges() { + log.V(1).Info("No delta found") + return nil + } + + if delta.IsAdded() { + log.V(1).Info("Delta found", "create", desiredSA.GetName()) + if err = r.client.Create(ctx, desiredSA); err != nil { + return + } + } + if delta.IsUpdated() { + log.V(1).Info("Delta found", "update", existingSA.GetName()) + rp := existingSA.DeepCopy() + if err = r.client.Update(ctx, rp); err != nil { + return + } + } + if delta.IsRemoved() { + log.V(1).Info("Delta found", "delete", existingSA.GetName()) + if err = r.client.Delete(ctx, existingSA); err != nil { + return + } + } + return nil +} + +func (r *ModelMeshServiceAccountReconciler) DeleteServiceAccount(ctx context.Context, isvcNamespace string) error { + return r.serviceAccountHandler.DeleteServiceAccount(ctx, types.NamespacedName{Name: modelMeshServiceAccountName, Namespace: isvcNamespace}) +} diff --git a/controllers/reconcilers/reconciler.go b/controllers/reconcilers/reconciler.go index 0bcd742c..f98c4628 100644 --- a/controllers/reconcilers/reconciler.go +++ b/controllers/reconcilers/reconciler.go @@ -1,5 +1,26 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package reconcilers +import ( + "context" + "github.com/go-logr/logr" + kservev1beta1 "github.com/kserve/kserve/pkg/apis/serving/v1beta1" +) + type Reconciler interface { - Reconcile() error + Reconcile(ctx context.Context, log logr.Logger, isvc *kservev1beta1.InferenceService) error } diff --git a/controllers/resources/clusterrolebinding.go b/controllers/resources/clusterrolebinding.go new file mode 100644 index 00000000..fcac4e45 --- /dev/null +++ b/controllers/resources/clusterrolebinding.go @@ -0,0 +1,69 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resources + +import ( + "context" + "fmt" + "github.com/go-logr/logr" + v1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type ClusterRoleBindingHandler interface { + FetchClusterRoleBinding(ctx context.Context, log logr.Logger, key types.NamespacedName) (*v1.ClusterRoleBinding, error) + DeleteClusterRoleBinding(ctx context.Context, key types.NamespacedName) error +} + +type clusterRoleBindingHandler struct { + client client.Client +} + +func NewClusterRoleBindingHandler(client client.Client) ClusterRoleBindingHandler { + return &clusterRoleBindingHandler{ + client: client, + } +} + +func (r *clusterRoleBindingHandler) FetchClusterRoleBinding(ctx context.Context, log logr.Logger, key types.NamespacedName) (*v1.ClusterRoleBinding, error) { + clusterRoleBinding := &v1.ClusterRoleBinding{} + err := r.client.Get(ctx, key, clusterRoleBinding) + if err != nil && errors.IsNotFound(err) { + log.V(1).Info("ClusterRoleBinding not found.") + return nil, nil + } else if err != nil { + return nil, err + } + log.V(1).Info("Successfully fetch deployed ClusterRoleBinding") + return clusterRoleBinding, nil +} + +func (r *clusterRoleBindingHandler) DeleteClusterRoleBinding(ctx context.Context, key types.NamespacedName) error { + clusterRoleBinding := &v1.ClusterRoleBinding{} + err := r.client.Get(ctx, key, clusterRoleBinding) + if err != nil { + if errors.IsNotFound(err) { + return nil + } + return err + } + if err = r.client.Delete(ctx, clusterRoleBinding); err != nil { + return fmt.Errorf("failed to delete ClusterRoleBinding: %w", err) + } + return nil +} diff --git a/controllers/resources/networkpolicy.go b/controllers/resources/networkpolicy.go new file mode 100644 index 00000000..28547e66 --- /dev/null +++ b/controllers/resources/networkpolicy.go @@ -0,0 +1,69 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resources + +import ( + "context" + "fmt" + "github.com/go-logr/logr" + "k8s.io/api/networking/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type NetworkPolicyHandler interface { + FetchNetworkPolicy(ctx context.Context, log logr.Logger, key types.NamespacedName) (*v1.NetworkPolicy, error) + DeleteNetworkPolicy(ctx context.Context, key types.NamespacedName) error +} + +type networkPolicyHandler struct { + client client.Client +} + +func NewNetworkPolicyHandler(client client.Client) NetworkPolicyHandler { + return &networkPolicyHandler{ + client: client, + } +} + +func (r *networkPolicyHandler) FetchNetworkPolicy(ctx context.Context, log logr.Logger, key types.NamespacedName) (*v1.NetworkPolicy, error) { + networkPolicy := &v1.NetworkPolicy{} + err := r.client.Get(ctx, key, networkPolicy) + if err != nil && errors.IsNotFound(err) { + log.V(1).Info("NetworkPolicy not found.") + return nil, nil + } else if err != nil { + return nil, err + } + log.V(1).Info("Successfully fetch deployed NetworkPolicy") + return networkPolicy, nil +} + +func (r *networkPolicyHandler) DeleteNetworkPolicy(ctx context.Context, key types.NamespacedName) error { + networkPolicy := &v1.NetworkPolicy{} + err := r.client.Get(ctx, key, networkPolicy) + if err != nil { + if errors.IsNotFound(err) { + return nil + } + return err + } + if err = r.client.Delete(ctx, networkPolicy); err != nil { + return fmt.Errorf("failed to delete NetworkPolicy: %w", err) + } + return nil +} diff --git a/controllers/resources/peerauthentication.go b/controllers/resources/peerauthentication.go new file mode 100644 index 00000000..b45bbb99 --- /dev/null +++ b/controllers/resources/peerauthentication.go @@ -0,0 +1,70 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resources + +import ( + "context" + "fmt" + "github.com/go-logr/logr" + istiosecv1beta1 "istio.io/client-go/pkg/apis/security/v1beta1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type PeerAuthenticationHandler interface { + FetchPeerAuthentication(ctx context.Context, log logr.Logger, key types.NamespacedName) (*istiosecv1beta1.PeerAuthentication, error) + DeletePeerAuthentication(ctx context.Context, key types.NamespacedName) error +} + +type peerAuthenticationHandler struct { + client client.Client +} + +func NewPeerAuthenticationHandler(client client.Client) PeerAuthenticationHandler { + return &peerAuthenticationHandler{ + client: client, + } +} + +func (r *peerAuthenticationHandler) FetchPeerAuthentication(ctx context.Context, log logr.Logger, key types.NamespacedName) (*istiosecv1beta1.PeerAuthentication, error) { + peerAuthentication := &istiosecv1beta1.PeerAuthentication{} + err := r.client.Get(ctx, key, peerAuthentication) + if err != nil && errors.IsNotFound(err) { + log.V(1).Info("PeerAuthentication not found.") + return nil, nil + } else if err != nil { + return nil, err + } + log.V(1).Info("Successfully fetch deployed PeerAuthentication") + return peerAuthentication, nil +} + +func (r *peerAuthenticationHandler) DeletePeerAuthentication(ctx context.Context, key types.NamespacedName) error { + peerAuthentication := &istiosecv1beta1.PeerAuthentication{} + err := r.client.Get(ctx, key, peerAuthentication) + if err != nil { + if errors.IsNotFound(err) { + return nil + } + return err + } + if err = r.client.Delete(ctx, peerAuthentication); err != nil { + return fmt.Errorf("failed to delete PeerAuthentication: %w", err) + } + + return nil +} diff --git a/controllers/resources/podmonitor.go b/controllers/resources/podmonitor.go new file mode 100644 index 00000000..091a9751 --- /dev/null +++ b/controllers/resources/podmonitor.go @@ -0,0 +1,69 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resources + +import ( + "context" + "fmt" + "github.com/go-logr/logr" + v1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type PodMonitorHandler interface { + FetchPodMonitor(ctx context.Context, log logr.Logger, key types.NamespacedName) (*v1.PodMonitor, error) + DeletePodMonitor(ctx context.Context, key types.NamespacedName) error +} + +type podMonitorHandler struct { + client client.Client +} + +func NewPodMonitorHandler(client client.Client) PodMonitorHandler { + return &podMonitorHandler{ + client: client, + } +} + +func (r *podMonitorHandler) FetchPodMonitor(ctx context.Context, log logr.Logger, key types.NamespacedName) (*v1.PodMonitor, error) { + podMonitor := &v1.PodMonitor{} + err := r.client.Get(ctx, key, podMonitor) + if err != nil && errors.IsNotFound(err) { + log.V(1).Info("PodMonitor not found.") + return nil, nil + } else if err != nil { + return nil, err + } + log.V(1).Info("Successfully fetch deployed PodMonitor") + return podMonitor, nil +} + +func (r *podMonitorHandler) DeletePodMonitor(ctx context.Context, key types.NamespacedName) error { + podMonitor := &v1.PodMonitor{} + err := r.client.Get(ctx, key, podMonitor) + if err != nil { + if errors.IsNotFound(err) { + return nil + } + return err + } + if err = r.client.Delete(ctx, podMonitor); err != nil { + return fmt.Errorf("failed to delete PodMonitor: %w", err) + } + return nil +} diff --git a/controllers/resources/rolebinding.go b/controllers/resources/rolebinding.go new file mode 100644 index 00000000..ef863f00 --- /dev/null +++ b/controllers/resources/rolebinding.go @@ -0,0 +1,69 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resources + +import ( + "context" + "fmt" + "github.com/go-logr/logr" + v1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type RoleBindingHandler interface { + FetchRoleBinding(ctx context.Context, log logr.Logger, key types.NamespacedName) (*v1.RoleBinding, error) + DeleteRoleBinding(ctx context.Context, key types.NamespacedName) error +} + +type roleBindingHandler struct { + client client.Client +} + +func NewRoleBindingHandler(client client.Client) RoleBindingHandler { + return &roleBindingHandler{ + client: client, + } +} + +func (r *roleBindingHandler) FetchRoleBinding(ctx context.Context, log logr.Logger, key types.NamespacedName) (*v1.RoleBinding, error) { + roleBinding := &v1.RoleBinding{} + err := r.client.Get(ctx, key, roleBinding) + if err != nil && errors.IsNotFound(err) { + log.V(1).Info("RoleBinding not found.") + return nil, nil + } else if err != nil { + return nil, err + } + log.V(1).Info("Successfully fetch deployed RoleBinding") + return roleBinding, nil +} + +func (r *roleBindingHandler) DeleteRoleBinding(ctx context.Context, key types.NamespacedName) error { + roleBinding := &v1.RoleBinding{} + err := r.client.Get(ctx, key, roleBinding) + if err != nil { + if errors.IsNotFound(err) { + return nil + } + return err + } + if err = r.client.Delete(ctx, roleBinding); err != nil { + return fmt.Errorf("failed to delete RoleBinding: %w", err) + } + return nil +} diff --git a/controllers/resources/route.go b/controllers/resources/route.go new file mode 100644 index 00000000..96a46fdf --- /dev/null +++ b/controllers/resources/route.go @@ -0,0 +1,70 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resources + +import ( + "context" + "fmt" + "github.com/go-logr/logr" + v1 "github.com/openshift/api/route/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// RouteHandler to provide route specific implementation. +type RouteHandler interface { + FetchRoute(ctx context.Context, log logr.Logger, key types.NamespacedName) (*v1.Route, error) + DeleteRoute(ctx context.Context, key types.NamespacedName) error +} + +type routeHandler struct { + client client.Client +} + +func NewRouteHandler(client client.Client) RouteHandler { + return &routeHandler{ + client: client, + } +} + +func (r *routeHandler) FetchRoute(ctx context.Context, log logr.Logger, key types.NamespacedName) (*v1.Route, error) { + route := &v1.Route{} + err := r.client.Get(ctx, key, route) + if err != nil && errors.IsNotFound(err) { + log.V(1).Info("Openshift Route not found.") + return nil, nil + } else if err != nil { + return nil, err + } + log.V(1).Info("Successfully fetch deployed Openshift Route") + return route, nil +} + +func (r *routeHandler) DeleteRoute(ctx context.Context, key types.NamespacedName) error { + route := &v1.Route{} + err := r.client.Get(ctx, key, route) + if err != nil { + if errors.IsNotFound(err) { + return nil + } + return err + } + if err = r.client.Delete(ctx, route); err != nil { + return fmt.Errorf("failed to delete route: %w", err) + } + return nil +} diff --git a/controllers/resources/service.go b/controllers/resources/service.go new file mode 100644 index 00000000..c3ac39ff --- /dev/null +++ b/controllers/resources/service.go @@ -0,0 +1,52 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resources + +import ( + "context" + "github.com/go-logr/logr" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type ServiceHandler interface { + FetchService(ctx context.Context, log logr.Logger, key types.NamespacedName) (*v1.Service, error) +} + +type serviceHandler struct { + client client.Client +} + +func NewServiceHandler(client client.Client) ServiceHandler { + return &serviceHandler{ + client: client, + } +} + +func (r *serviceHandler) FetchService(ctx context.Context, log logr.Logger, key types.NamespacedName) (*v1.Service, error) { + route := &v1.Service{} + err := r.client.Get(ctx, key, route) + if err != nil && errors.IsNotFound(err) { + log.V(1).Info("Service not found.") + return nil, nil + } else if err != nil { + return nil, err + } + log.V(1).Info("Successfully fetch deployed Service") + return route, nil +} diff --git a/controllers/resources/serviceaccount.go b/controllers/resources/serviceaccount.go new file mode 100644 index 00000000..659e7b43 --- /dev/null +++ b/controllers/resources/serviceaccount.go @@ -0,0 +1,69 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resources + +import ( + "context" + "fmt" + "github.com/go-logr/logr" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type ServiceAccountHandler interface { + FetchServiceAccount(ctx context.Context, log logr.Logger, key types.NamespacedName) (*v1.ServiceAccount, error) + DeleteServiceAccount(ctx context.Context, key types.NamespacedName) error +} + +type serviceAccountHandler struct { + client client.Client +} + +func NewServiceAccountHandler(client client.Client) ServiceAccountHandler { + return &serviceAccountHandler{ + client: client, + } +} + +func (r *serviceAccountHandler) FetchServiceAccount(ctx context.Context, log logr.Logger, key types.NamespacedName) (*v1.ServiceAccount, error) { + serviceAccount := &v1.ServiceAccount{} + err := r.client.Get(ctx, key, serviceAccount) + if err != nil && errors.IsNotFound(err) { + log.V(1).Info("Service account not found.") + return nil, nil + } else if err != nil { + return nil, err + } + log.V(1).Info("Successfully fetch deployed Service account") + return serviceAccount, nil +} + +func (r *serviceAccountHandler) DeleteServiceAccount(ctx context.Context, key types.NamespacedName) error { + serviceAccount := &v1.ServiceAccount{} + err := r.client.Get(ctx, key, serviceAccount) + if err != nil { + if errors.IsNotFound(err) { + return nil + } + return err + } + if err = r.client.Delete(ctx, serviceAccount); err != nil { + return fmt.Errorf("failed to delete ServiceAccount: %w", err) + } + return nil +} diff --git a/controllers/resources/servicemonitor.go b/controllers/resources/servicemonitor.go new file mode 100644 index 00000000..6ba078dd --- /dev/null +++ b/controllers/resources/servicemonitor.go @@ -0,0 +1,69 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resources + +import ( + "context" + "fmt" + "github.com/go-logr/logr" + v1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type ServiceMonitorHandler interface { + FetchServiceMonitor(ctx context.Context, log logr.Logger, key types.NamespacedName) (*v1.ServiceMonitor, error) + DeleteServiceMonitor(ctx context.Context, key types.NamespacedName) error +} + +type serviceMonitorHandler struct { + client client.Client +} + +func NewServiceMonitorHandler(client client.Client) ServiceMonitorHandler { + return &serviceMonitorHandler{ + client: client, + } +} + +func (r *serviceMonitorHandler) FetchServiceMonitor(ctx context.Context, log logr.Logger, key types.NamespacedName) (*v1.ServiceMonitor, error) { + serviceMonitor := &v1.ServiceMonitor{} + err := r.client.Get(ctx, key, serviceMonitor) + if err != nil && errors.IsNotFound(err) { + log.V(1).Info("ServiceMonitor not found.") + return nil, nil + } else if err != nil { + return nil, err + } + log.V(1).Info("Successfully fetch deployed ServiceMonitor") + return serviceMonitor, nil +} + +func (r *serviceMonitorHandler) DeleteServiceMonitor(ctx context.Context, key types.NamespacedName) error { + serviceMonitor := &v1.ServiceMonitor{} + err := r.client.Get(ctx, key, serviceMonitor) + if err != nil { + if errors.IsNotFound(err) { + return nil + } + return err + } + if err = r.client.Delete(ctx, serviceMonitor); err != nil { + return fmt.Errorf("failed to delete ServiceMonitor: %w", err) + } + return nil +} diff --git a/controllers/resources/smmr.go b/controllers/resources/smmr.go new file mode 100644 index 00000000..ccd55745 --- /dev/null +++ b/controllers/resources/smmr.go @@ -0,0 +1,77 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resources + +import ( + "context" + "fmt" + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + v1 "maistra.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type ServiceMeshMemberRollHandler interface { + FetchSMMR(ctx context.Context, log logr.Logger, key types.NamespacedName) (*v1.ServiceMeshMemberRoll, error) + RemoveMemberFromSMMR(ctx context.Context, key types.NamespacedName, member string) error +} + +type serviceMeshMemberRollHandler struct { + client client.Client +} + +func NewServiceMeshMemberRole(client client.Client) ServiceMeshMemberRollHandler { + return &serviceMeshMemberRollHandler{ + client: client, + } +} + +func (r *serviceMeshMemberRollHandler) FetchSMMR(ctx context.Context, log logr.Logger, key types.NamespacedName) (*v1.ServiceMeshMemberRoll, error) { + smmr := &v1.ServiceMeshMemberRoll{} + err := r.client.Get(ctx, key, smmr) + if err != nil && errors.IsNotFound(err) { + log.V(1).Info("ServiceMeshMemberRole not found.") + return nil, nil + } else if err != nil { + return nil, err + } + log.V(1).Info("Successfully fetch deployed ServiceMeshMemberRole") + return smmr, nil +} + +func (r *serviceMeshMemberRollHandler) RemoveMemberFromSMMR(ctx context.Context, key types.NamespacedName, member string) error { + smmr := &v1.ServiceMeshMemberRoll{} + err := r.client.Get(ctx, key, smmr) + if err != nil { + if errors.IsNotFound(err) { + return nil + } + return err + } + serviceMeshMemberList := smmr.Spec.Members + // remove the namespace from the list + for i, smmrMember := range serviceMeshMemberList { + if smmrMember == member { + serviceMeshMemberList = append(serviceMeshMemberList[:i], serviceMeshMemberList[i+1:]...) + } + } + smmr.Spec.Members = serviceMeshMemberList + if err = r.client.Update(ctx, smmr); err != nil { + return fmt.Errorf("failed to remove member from ServiceMeshMemberRole: %w", err) + } + return nil +} diff --git a/controllers/resources/telemetry.go b/controllers/resources/telemetry.go new file mode 100644 index 00000000..5dbbeceb --- /dev/null +++ b/controllers/resources/telemetry.go @@ -0,0 +1,70 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resources + +import ( + "context" + "fmt" + "github.com/go-logr/logr" + telemetryv1alpha1 "istio.io/client-go/pkg/apis/telemetry/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// TelemetryHandler to provide telemetry specific implementation. +type TelemetryHandler interface { + FetchTelemetry(ctx context.Context, log logr.Logger, key types.NamespacedName) (*telemetryv1alpha1.Telemetry, error) + DeleteTelemetry(ctx context.Context, key types.NamespacedName) error +} + +type telemetryHandler struct { + client client.Client +} + +func NewTelemetryHandler(client client.Client) TelemetryHandler { + return &telemetryHandler{ + client: client, + } +} + +func (r *telemetryHandler) FetchTelemetry(ctx context.Context, log logr.Logger, key types.NamespacedName) (*telemetryv1alpha1.Telemetry, error) { + telemetry := &telemetryv1alpha1.Telemetry{} + err := r.client.Get(ctx, key, telemetry) + if err != nil && errors.IsNotFound(err) { + log.V(1).Info("Istio Telemetry not found.") + return nil, nil + } else if err != nil { + return nil, err + } + log.V(1).Info("Successfully fetch deployed Istio Telemetry") + return telemetry, nil +} + +func (r *telemetryHandler) DeleteTelemetry(ctx context.Context, key types.NamespacedName) error { + telemetry := &telemetryv1alpha1.Telemetry{} + err := r.client.Get(ctx, key, telemetry) + if err != nil { + if errors.IsNotFound(err) { + return nil + } + return err + } + if err = r.client.Delete(ctx, telemetry); err != nil { + return fmt.Errorf("failed to delete Istio Telemetry: %w", err) + } + return nil +} diff --git a/controllers/suite_test.go b/controllers/suite_test.go index 5c94de7a..8db1496c 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -70,6 +70,7 @@ const ( InferenceService1 = "./testdata/deploy/openvino-inference-service-1.yaml" InferenceServiceNoRuntime = "./testdata/deploy/openvino-inference-service-no-runtime.yaml" KserveInferenceServicePath1 = "./testdata/deploy/kserve-openvino-inference-service-1.yaml" + InferenceServiceConfigPath1 = "./testdata/configmaps/inferenceservice-config.yaml" ExpectedRoutePath = "./testdata/results/example-onnx-mnist-route.yaml" ExpectedRouteNoRuntimePath = "./testdata/results/example-onnx-mnist-no-runtime-route.yaml" timeout = time.Second * 20 @@ -134,12 +135,12 @@ var _ = BeforeSuite(func() { Expect(err).NotTo(HaveOccurred()) - err = (&OpenshiftInferenceServiceReconciler{ - Client: cli, - Log: ctrl.Log.WithName("controllers").WithName("inferenceservice-controller"), - Scheme: scheme.Scheme, - MeshDisabled: false, - }).SetupWithManager(mgr) + err = (NewOpenshiftInferenceServiceReconciler( + mgr.GetClient(), + scheme.Scheme, + ctrl.Log.WithName("controllers").WithName("InferenceService-controller"), + false)). + SetupWithManager(mgr) Expect(err).ToNot(HaveOccurred()) err = (&MonitoringReconciler{ diff --git a/controllers/utils/utils.go b/controllers/utils/utils.go index afc4a9c9..526384da 100644 --- a/controllers/utils/utils.go +++ b/controllers/utils/utils.go @@ -1,6 +1,22 @@ package utils -import "reflect" +import ( + kservev1beta1 "github.com/kserve/kserve/pkg/apis/serving/v1beta1" + "reflect" +) + +const ( + inferenceServiceDeploymentModeAnnotation = "serving.kserve.io/deploymentMode" + inferenceServiceDeploymentModeAnnotationValue = "ModelMesh" +) + +func IsDeploymentModeForIsvcModelMesh(isvc *kservev1beta1.InferenceService) bool { + value, exists := isvc.Annotations[inferenceServiceDeploymentModeAnnotation] + if exists && value == inferenceServiceDeploymentModeAnnotationValue { + return true + } + return false +} func IsNil(i any) bool { return reflect.ValueOf(i).IsNil() diff --git a/main.go b/main.go index b0065068..e1c63c11 100644 --- a/main.go +++ b/main.go @@ -139,12 +139,12 @@ func main() { } //Setup InferenceService controller - if err = (&controllers.OpenshiftInferenceServiceReconciler{ - Client: mgr.GetClient(), - Log: ctrl.Log.WithName("controllers").WithName("InferenceService"), - Scheme: mgr.GetScheme(), - MeshDisabled: getEnvAsBool("MESH_DISABLED", false), - }).SetupWithManager(mgr); err != nil { + if err = (controllers.NewOpenshiftInferenceServiceReconciler( + mgr.GetClient(), + mgr.GetScheme(), + ctrl.Log.WithName("controllers").WithName("InferenceService"), + getEnvAsBool("MESH_DISABLED", false))). + SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "InferenceService") os.Exit(1) }