diff --git a/config/overlays/odh/inferenceservice-config-patch.yaml b/config/overlays/odh/inferenceservice-config-patch.yaml index 3ff7f723c9..5f9791ef78 100644 --- a/config/overlays/odh/inferenceservice-config-patch.yaml +++ b/config/overlays/odh/inferenceservice-config-patch.yaml @@ -22,7 +22,6 @@ data: "localGateway" : "istio-system/kserve-local-gateway", "localGatewayService" : "kserve-local-gateway.istio-system.svc.cluster.local", "ingressDomain" : "example.com", - "ingressClassName" : "istio", "domainTemplate": "{{ .Name }}-{{ .Namespace }}.{{ .IngressDomain }}", "urlScheme": "https", "disableIstioVirtualHost": false, diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 390ac4088b..b6318be951 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -38,6 +38,7 @@ var ( KnativeServingAPIGroupName = KnativeServingAPIGroupNamePrefix + ".dev" KServeNamespace = getEnvOrDefault("POD_NAMESPACE", "kserve") KServeDefaultVersion = "v0.5.0" + KserveServiceAccountName = "kserve-sa" ) // InferenceService Constants @@ -122,6 +123,7 @@ const ( NetworkVisibility = "networking.kserve.io/visibility" ClusterLocalVisibility = "cluster-local" ClusterLocalDomain = "svc.cluster.local" + ODHKserveRawAuth = "networking.kserve.io/odh-auth" ) // StorageSpec Constants @@ -414,6 +416,16 @@ const ( SupportedModelMLFlow = "mlflow" ) +// opendatahub rawDeployment Auth +const ( + OauthProxyImage = "registry.redhat.io/openshift4/ose-oauth-proxy@sha256:4bef31eb993feb6f1096b51b4876c65a6fb1f4401fee97fa4f4542b6b7c9bc46" + OauthProxyPort = 8443 + OauthProxyResourceMemoryLimit = "256Mi" + OauthProxyResourceCPULimit = "100m" + OauthProxyResourceMemoryRequest = "256Mi" + OauthProxyResourceCPURequest = "100m" +) + type ProtocolVersion int const ( diff --git a/pkg/controller/v1beta1/inferenceservice/rawkube_controller_test.go b/pkg/controller/v1beta1/inferenceservice/rawkube_controller_test.go index 789983c704..cfa95acd93 100644 --- a/pkg/controller/v1beta1/inferenceservice/rawkube_controller_test.go +++ b/pkg/controller/v1beta1/inferenceservice/rawkube_controller_test.go @@ -689,6 +689,8 @@ var _ = Describe("v1beta1 inference service controller", func() { FSGroupChangePolicy: nil, SeccompProfile: nil, }, + ServiceAccountName: constants.KserveServiceAccountName, + DeprecatedServiceAccount: constants.KserveServiceAccountName, }, }, // This is now customized and different from defaults set via `setDefaultDeploymentSpec`. @@ -1091,6 +1093,8 @@ var _ = Describe("v1beta1 inference service controller", func() { FSGroupChangePolicy: nil, SeccompProfile: nil, }, + ServiceAccountName: constants.KserveServiceAccountName, + DeprecatedServiceAccount: constants.KserveServiceAccountName, }, }, Strategy: appsv1.DeploymentStrategy{ @@ -1264,6 +1268,109 @@ var _ = Describe("v1beta1 inference service controller", func() { Eventually(func() error { return k8sClient.Get(context.TODO(), predictorHPAKey, actualHPA) }, timeout). Should(HaveOccurred()) }) + It("Should have no ingress created if labeled as cluster-local", func() { + By("By creating a new InferenceService") + // Create configmap + var configMap = &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: constants.InferenceServiceConfigMapName, + Namespace: constants.KServeNamespace, + }, + Data: configs, + } + Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred()) + defer k8sClient.Delete(context.TODO(), configMap) + // Create ServingRuntime + servingRuntime := &v1alpha1.ServingRuntime{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tf-serving-raw", + Namespace: "default", + }, + Spec: v1alpha1.ServingRuntimeSpec{ + SupportedModelFormats: []v1alpha1.SupportedModelFormat{ + { + Name: "tensorflow", + Version: proto.String("1"), + AutoSelect: proto.Bool(true), + }, + }, + ServingRuntimePodSpec: v1alpha1.ServingRuntimePodSpec{ + Containers: []v1.Container{ + { + Name: "kserve-container", + Image: "tensorflow/serving:1.14.0", + Command: []string{"/usr/bin/tensorflow_model_server"}, + Args: []string{ + "--port=9000", + "--rest_api_port=8080", + "--model_base_path=/mnt/models", + "--rest_api_timeout_in_ms=60000", + }, + Resources: defaultResource, + }, + }, + }, + Disabled: proto.Bool(false), + }, + } + k8sClient.Create(context.TODO(), servingRuntime) + defer k8sClient.Delete(context.TODO(), servingRuntime) + serviceName := "raw-cluster-local" + var expectedRequest = reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: "default"}} + var serviceKey = expectedRequest.NamespacedName + var storageUri = "s3://test/mnist/export" + ctx := context.Background() + isvc := &v1beta1.InferenceService{ + ObjectMeta: metav1.ObjectMeta{ + Name: serviceKey.Name, + Namespace: serviceKey.Namespace, + Annotations: map[string]string{ + "serving.kserve.io/deploymentMode": "RawDeployment", + "serving.kserve.io/autoscalerClass": "hpa", + "serving.kserve.io/metrics": "cpu", + "serving.kserve.io/targetUtilizationPercentage": "75", + }, + Labels: map[string]string{ + "networking.kserve.io/visibility": "cluster-local", + }, + }, + Spec: v1beta1.InferenceServiceSpec{ + Predictor: v1beta1.PredictorSpec{ + ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{ + MinReplicas: v1beta1.GetIntReference(1), + MaxReplicas: 3, + }, + Tensorflow: &v1beta1.TFServingSpec{ + PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{ + StorageURI: &storageUri, + RuntimeVersion: proto.String("1.14.0"), + Container: v1.Container{ + Name: constants.InferenceServiceContainerName, + Resources: defaultResource, + }, + }, + }, + }, + }, + } + isvc.DefaultInferenceService(nil, nil) + Expect(k8sClient.Create(ctx, isvc)).Should(Succeed()) + + inferenceService := &v1beta1.InferenceService{} + + Eventually(func() bool { + err := k8sClient.Get(ctx, serviceKey, inferenceService) + if err != nil { + return false + } + return true + }, timeout, interval).Should(BeTrue()) + actualIngress := &netv1.Ingress{} + predictorIngressKey := types.NamespacedName{Name: serviceKey.Name, + Namespace: serviceKey.Namespace} + Eventually(func() error { return k8sClient.Get(context.TODO(), predictorIngressKey, actualIngress) }, timeout). + ShouldNot(Succeed()) + }) }) Context("When creating inference service with raw kube predictor and empty ingressClassName", func() { configs := map[string]string{ @@ -1465,6 +1572,8 @@ var _ = Describe("v1beta1 inference service controller", func() { FSGroupChangePolicy: nil, SeccompProfile: nil, }, + ServiceAccountName: constants.KserveServiceAccountName, + DeprecatedServiceAccount: constants.KserveServiceAccountName, }, }, Strategy: appsv1.DeploymentStrategy{ @@ -1898,6 +2007,8 @@ var _ = Describe("v1beta1 inference service controller", func() { FSGroupChangePolicy: nil, SeccompProfile: nil, }, + ServiceAccountName: constants.KserveServiceAccountName, + DeprecatedServiceAccount: constants.KserveServiceAccountName, }, }, Strategy: appsv1.DeploymentStrategy{ @@ -2130,4 +2241,469 @@ var _ = Describe("v1beta1 inference service controller", func() { Expect(actualHPA.Spec).To(gomega.Equal(expectedHPA.Spec)) }) }) + Context("When creating an inferenceservice with raw kube predictor and ODH auth enabled", func() { + configs := map[string]string{ + "explainers": `{ + "alibi": { + "image": "kserve/alibi-explainer", + "defaultImageVersion": "latest" + } + }`, + "ingress": `{ + "ingressGateway": "knative-serving/knative-ingress-gateway", + "ingressService": "test-destination", + "localGateway": "knative-serving/knative-local-gateway", + "localGatewayService": "knative-local-gateway.istio-system.svc.cluster.local" + }`, + "storageInitializer": `{ + "image" : "kserve/storage-initializer:latest", + "memoryRequest": "100Mi", + "memoryLimit": "1Gi", + "cpuRequest": "100m", + "cpuLimit": "1", + "CaBundleConfigMapName": "", + "caBundleVolumeMountPath": "/etc/ssl/custom-certs", + "enableDirectPvcVolumeMount": false + }`, + } + + It("Should have ingress/service/deployment/hpa created", func() { + By("By creating a new InferenceService") + // Create configmap + var configMap = &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: constants.InferenceServiceConfigMapName, + Namespace: constants.KServeNamespace, + }, + Data: configs, + } + Expect(k8sClient.Create(context.TODO(), configMap)).NotTo(HaveOccurred()) + defer k8sClient.Delete(context.TODO(), configMap) + // Create ServingRuntime + servingRuntime := &v1alpha1.ServingRuntime{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tf-serving-raw", + Namespace: "default", + }, + Spec: v1alpha1.ServingRuntimeSpec{ + SupportedModelFormats: []v1alpha1.SupportedModelFormat{ + { + Name: "tensorflow", + Version: proto.String("1"), + AutoSelect: proto.Bool(true), + }, + }, + ServingRuntimePodSpec: v1alpha1.ServingRuntimePodSpec{ + Containers: []v1.Container{ + { + Name: "kserve-container", + Image: "tensorflow/serving:1.14.0", + Command: []string{"/usr/bin/tensorflow_model_server"}, + Args: []string{ + "--port=9000", + "--rest_api_port=8080", + "--model_base_path=/mnt/models", + "--rest_api_timeout_in_ms=60000", + }, + Resources: defaultResource, + }, + }, + }, + Disabled: proto.Bool(false), + }, + } + k8sClient.Create(context.TODO(), servingRuntime) + defer k8sClient.Delete(context.TODO(), servingRuntime) + serviceName := "raw-auth" + var expectedRequest = reconcile.Request{NamespacedName: types.NamespacedName{Name: serviceName, Namespace: "default"}} + var serviceKey = expectedRequest.NamespacedName + var storageUri = "s3://test/mnist/export" + ctx := context.Background() + isvc := &v1beta1.InferenceService{ + ObjectMeta: metav1.ObjectMeta{ + Name: serviceKey.Name, + Namespace: serviceKey.Namespace, + Annotations: map[string]string{ + "serving.kserve.io/deploymentMode": "RawDeployment", + }, + Labels: map[string]string{ + "networking.kserve.io/odh-auth": "true", + }, + }, + Spec: v1beta1.InferenceServiceSpec{ + Predictor: v1beta1.PredictorSpec{ + ComponentExtensionSpec: v1beta1.ComponentExtensionSpec{ + MinReplicas: v1beta1.GetIntReference(1), + MaxReplicas: 3, + }, + Tensorflow: &v1beta1.TFServingSpec{ + PredictorExtensionSpec: v1beta1.PredictorExtensionSpec{ + StorageURI: &storageUri, + RuntimeVersion: proto.String("1.14.0"), + Container: v1.Container{ + Name: constants.InferenceServiceContainerName, + Resources: defaultResource, + }, + }, + }, + }, + }, + } + isvc.DefaultInferenceService(nil, nil) + Expect(k8sClient.Create(ctx, isvc)).Should(Succeed()) + + inferenceService := &v1beta1.InferenceService{} + + Eventually(func() bool { + err := k8sClient.Get(ctx, serviceKey, inferenceService) + if err != nil { + return false + } + return true + }, timeout, interval).Should(BeTrue()) + + actualDeployment := &appsv1.Deployment{} + predictorDeploymentKey := types.NamespacedName{Name: constants.PredictorServiceName(serviceKey.Name), + Namespace: serviceKey.Namespace} + Eventually(func() error { return k8sClient.Get(context.TODO(), predictorDeploymentKey, actualDeployment) }, timeout). + Should(Succeed()) + var replicas int32 = 1 + var revisionHistory int32 = 10 + var progressDeadlineSeconds int32 = 600 + var gracePeriod int64 = 30 + expectedDeployment := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: predictorDeploymentKey.Name, + Namespace: predictorDeploymentKey.Namespace, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: &replicas, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "isvc." + predictorDeploymentKey.Name, + }, + }, + Template: v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Name: predictorDeploymentKey.Name, + Namespace: "default", + Labels: map[string]string{ + "app": "isvc." + predictorDeploymentKey.Name, + constants.KServiceComponentLabel: constants.Predictor.String(), + constants.InferenceServicePodLabelKey: serviceName, + "serving.kserve.io/inferenceservice": serviceName, + "networking.kserve.io/odh-auth": "true", + }, + Annotations: map[string]string{ + constants.StorageInitializerSourceUriInternalAnnotationKey: *isvc.Spec.Predictor.Model.StorageURI, + "serving.kserve.io/deploymentMode": "RawDeployment", + "service.beta.openshift.io/serving-cert-secret-name": predictorDeploymentKey.Name, + }, + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Image: "tensorflow/serving:" + + *isvc.Spec.Predictor.Model.RuntimeVersion, + Name: constants.InferenceServiceContainerName, + Command: []string{v1beta1.TensorflowEntrypointCommand}, + Args: []string{ + "--port=" + v1beta1.TensorflowServingGRPCPort, + "--rest_api_port=" + v1beta1.TensorflowServingRestPort, + "--model_base_path=" + constants.DefaultModelLocalMountPath, + "--rest_api_timeout_in_ms=60000", + }, + Resources: defaultResource, + ReadinessProbe: &v1.Probe{ + ProbeHandler: v1.ProbeHandler{ + TCPSocket: &v1.TCPSocketAction{ + Port: intstr.IntOrString{ + IntVal: 8080, + }, + }, + }, + InitialDelaySeconds: 0, + TimeoutSeconds: 1, + PeriodSeconds: 10, + SuccessThreshold: 1, + FailureThreshold: 3, + }, + TerminationMessagePath: "/dev/termination-log", + TerminationMessagePolicy: "File", + ImagePullPolicy: "IfNotPresent", + }, + { + Name: "oauth-proxy", + Image: constants.OauthProxyImage, + Args: []string{ + `--https-address=:8443`, + `--provider=openshift`, + `--openshift-service-account=kserve-sa`, + `--upstream=http://localhost:8080`, + `--tls-cert=/etc/tls/private/tls.crt`, + `--tls-key=/etc/tls/private/tls.key`, + `--cookie-secret=SECRET`, + `--openshift-delegate-urls={"/": {"namespace": "` + serviceKey.Namespace + `", "resource": "services", "verb": "get"}}`, + `--openshift-sar={"namespace": "` + serviceKey.Namespace + `", "resource": "services", "verb": "get"}`, + `--skip-auth-regex="(^/metrics|^/apis/v1beta1/healthz)"`, + }, + Ports: []v1.ContainerPort{ + { + ContainerPort: constants.OauthProxyPort, + Name: "https", + Protocol: v1.ProtocolTCP, + }, + }, + LivenessProbe: &v1.Probe{ + ProbeHandler: v1.ProbeHandler{ + HTTPGet: &v1.HTTPGetAction{ + Path: "/oauth/healthz", + Port: intstr.FromInt(constants.OauthProxyPort), + Scheme: v1.URISchemeHTTPS, + }, + }, + InitialDelaySeconds: 30, + TimeoutSeconds: 1, + PeriodSeconds: 5, + SuccessThreshold: 1, + FailureThreshold: 3, + }, + ReadinessProbe: &v1.Probe{ + ProbeHandler: v1.ProbeHandler{ + HTTPGet: &v1.HTTPGetAction{ + Path: "/oauth/healthz", + Port: intstr.FromInt(constants.OauthProxyPort), + Scheme: v1.URISchemeHTTPS, + }, + }, + InitialDelaySeconds: 5, + TimeoutSeconds: 1, + PeriodSeconds: 5, + SuccessThreshold: 1, + FailureThreshold: 3, + }, + Resources: v1.ResourceRequirements{ + Limits: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse(constants.OauthProxyResourceCPULimit), + v1.ResourceMemory: resource.MustParse(constants.OauthProxyResourceMemoryLimit), + }, + Requests: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse(constants.OauthProxyResourceCPURequest), + v1.ResourceMemory: resource.MustParse(constants.OauthProxyResourceMemoryRequest), + }, + }, + VolumeMounts: []v1.VolumeMount{ + { + Name: "proxy-tls", + MountPath: "/etc/tls/private", + }, + }, + TerminationMessagePath: "/dev/termination-log", + TerminationMessagePolicy: "File", + ImagePullPolicy: "IfNotPresent", + }, + }, + Volumes: []v1.Volume{ + { + Name: "proxy-tls", + VolumeSource: v1.VolumeSource{ + Secret: &v1.SecretVolumeSource{ + SecretName: predictorDeploymentKey.Name, + DefaultMode: func(i int32) *int32 { return &i }(420), + }, + }, + }, + }, + ServiceAccountName: constants.KserveServiceAccountName, + DeprecatedServiceAccount: constants.KserveServiceAccountName, + SchedulerName: "default-scheduler", + RestartPolicy: "Always", + TerminationGracePeriodSeconds: &gracePeriod, + DNSPolicy: "ClusterFirst", + SecurityContext: &v1.PodSecurityContext{ + SELinuxOptions: nil, + WindowsOptions: nil, + RunAsUser: nil, + RunAsGroup: nil, + RunAsNonRoot: nil, + SupplementalGroups: nil, + FSGroup: nil, + Sysctls: nil, + FSGroupChangePolicy: nil, + SeccompProfile: nil, + }, + }, + }, + Strategy: appsv1.DeploymentStrategy{ + Type: "RollingUpdate", + RollingUpdate: &appsv1.RollingUpdateDeployment{ + MaxUnavailable: &intstr.IntOrString{Type: 1, IntVal: 0, StrVal: "25%"}, + MaxSurge: &intstr.IntOrString{Type: 1, IntVal: 0, StrVal: "25%"}, + }, + }, + RevisionHistoryLimit: &revisionHistory, + ProgressDeadlineSeconds: &progressDeadlineSeconds, + }, + } + Expect(actualDeployment.Spec).To(gomega.Equal(expectedDeployment.Spec)) + + //check service + actualService := &v1.Service{} + predictorServiceKey := types.NamespacedName{Name: constants.PredictorServiceName(serviceKey.Name), + Namespace: serviceKey.Namespace} + Eventually(func() error { return k8sClient.Get(context.TODO(), predictorServiceKey, actualService) }, timeout). + Should(Succeed()) + + expectedService := &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: predictorServiceKey.Name, + Namespace: predictorServiceKey.Namespace, + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + { + Name: constants.PredictorServiceName(serviceName), + Protocol: "TCP", + Port: 80, + TargetPort: intstr.IntOrString{Type: 0, IntVal: 8080, StrVal: ""}, + }, + { + Name: "https", + Protocol: "TCP", + Port: 8443, + TargetPort: intstr.IntOrString{Type: intstr.String, StrVal: "https"}, + }, + }, + Type: "ClusterIP", + SessionAffinity: "None", + Selector: map[string]string{ + "app": fmt.Sprintf("isvc.%s", constants.PredictorServiceName(serviceName)), + }, + }, + } + actualService.Spec.ClusterIP = "" + actualService.Spec.ClusterIPs = nil + actualService.Spec.IPFamilies = nil + actualService.Spec.IPFamilyPolicy = nil + actualService.Spec.InternalTrafficPolicy = nil + Expect(actualService.Spec).To(gomega.Equal(expectedService.Spec)) + + //check isvc status + updatedDeployment := actualDeployment.DeepCopy() + updatedDeployment.Status.Conditions = []appsv1.DeploymentCondition{ + { + Type: appsv1.DeploymentAvailable, + Status: v1.ConditionTrue, + }, + } + Expect(k8sClient.Status().Update(context.TODO(), updatedDeployment)).NotTo(gomega.HaveOccurred()) + + //check ingress + pathType := netv1.PathTypePrefix + actualIngress := &netv1.Ingress{} + predictorIngressKey := types.NamespacedName{Name: serviceKey.Name, + Namespace: serviceKey.Namespace} + Eventually(func() error { return k8sClient.Get(context.TODO(), predictorIngressKey, actualIngress) }, timeout). + Should(Succeed()) + expectedIngress := netv1.Ingress{ + Spec: netv1.IngressSpec{ + Rules: []netv1.IngressRule{ + { + Host: "raw-auth-default.example.com", + IngressRuleValue: netv1.IngressRuleValue{ + HTTP: &netv1.HTTPIngressRuleValue{ + Paths: []netv1.HTTPIngressPath{ + { + Path: "/", + PathType: &pathType, + Backend: netv1.IngressBackend{ + Service: &netv1.IngressServiceBackend{ + Name: "raw-auth-predictor", + Port: netv1.ServiceBackendPort{ + Number: 8443, + }, + }, + }, + }, + }, + }, + }, + }, + { + Host: "raw-auth-predictor-default.example.com", + IngressRuleValue: netv1.IngressRuleValue{ + HTTP: &netv1.HTTPIngressRuleValue{ + Paths: []netv1.HTTPIngressPath{ + { + Path: "/", + PathType: &pathType, + Backend: netv1.IngressBackend{ + Service: &netv1.IngressServiceBackend{ + Name: "raw-auth-predictor", + Port: netv1.ServiceBackendPort{ + Number: 8443, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + Expect(actualIngress.Spec).To(gomega.Equal(expectedIngress.Spec)) + // verify if InferenceService status is updated + expectedIsvcStatus := v1beta1.InferenceServiceStatus{ + Status: duckv1.Status{ + Conditions: duckv1.Conditions{ + { + Type: v1beta1.IngressReady, + Status: "True", + }, + { + Type: v1beta1.PredictorReady, + Status: "True", + }, + { + Type: apis.ConditionReady, + Status: "True", + }, + }, + }, + URL: &apis.URL{ + Scheme: "http", + Host: "raw-auth-default.example.com", + }, + Address: &duckv1.Addressable{ + URL: &apis.URL{ + Scheme: "http", + Host: fmt.Sprintf("%s-predictor.%s.svc.cluster.local", serviceKey.Name, serviceKey.Namespace), + }, + }, + Components: map[v1beta1.ComponentType]v1beta1.ComponentStatusSpec{ + v1beta1.PredictorComponent: { + LatestCreatedRevision: "", + URL: &apis.URL{ + Scheme: "http", + Host: "raw-auth-predictor-default.example.com", + }, + }, + }, + ModelStatus: v1beta1.ModelStatus{ + TransitionStatus: "InProgress", + ModelRevisionStates: &v1beta1.ModelRevisionStates{TargetModelState: "Pending"}, + }, + } + Eventually(func() string { + isvc := &v1beta1.InferenceService{} + if err := k8sClient.Get(context.TODO(), serviceKey, isvc); err != nil { + return err.Error() + } + return cmp.Diff(&expectedIsvcStatus, &isvc.Status, cmpopts.IgnoreTypes(apis.VolatileTime{})) + }, timeout).Should(gomega.BeEmpty()) + + }) + }) }) diff --git a/pkg/controller/v1beta1/inferenceservice/reconcilers/deployment/deployment_reconciler.go b/pkg/controller/v1beta1/inferenceservice/reconcilers/deployment/deployment_reconciler.go index 0ba5b58d8d..09dfc2f5ce 100644 --- a/pkg/controller/v1beta1/inferenceservice/reconcilers/deployment/deployment_reconciler.go +++ b/pkg/controller/v1beta1/inferenceservice/reconcilers/deployment/deployment_reconciler.go @@ -18,6 +18,9 @@ package deployment import ( "context" + "k8s.io/apimachinery/pkg/api/resource" + "strconv" + "strings" "github.com/google/go-cmp/cmp/cmpopts" "github.com/kserve/kserve/pkg/apis/serving/v1beta1" @@ -44,6 +47,10 @@ type DeploymentReconciler struct { componentExt *v1beta1.ComponentExtensionSpec } +const ( + tlsVolumeName = "proxy-tls" +) + func NewDeploymentReconciler(client kclient.Client, scheme *runtime.Scheme, componentMeta metav1.ObjectMeta, @@ -63,6 +70,24 @@ func createRawDeployment(componentMeta metav1.ObjectMeta, podMetadata := componentMeta podMetadata.Labels["app"] = constants.GetRawServiceLabel(componentMeta.Name) setDefaultPodSpec(podSpec) + if val, ok := componentMeta.Labels[constants.ODHKserveRawAuth]; ok && val == "true" { + kserveContainerPort := GetKServeContainerPort(podSpec) + if kserveContainerPort == "" { + kserveContainerPort = constants.InferenceServiceDefaultHttpPort + } + oauthProxyContainer := generateOauthProxyContainer(kserveContainerPort, componentMeta.Namespace) + podSpec.Containers = append(podSpec.Containers, oauthProxyContainer) + tlsSecretVolume := corev1.Volume{ + Name: tlsVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: componentMeta.Name, + DefaultMode: func(i int32) *int32 { return &i }(420), // Directly use a pointer + }, + }, + } + podSpec.Volumes = append(podSpec.Volumes, tlsSecretVolume) + } deployment := &appsv1.Deployment{ ObjectMeta: componentMeta, Spec: appsv1.DeploymentSpec{ @@ -84,6 +109,90 @@ func createRawDeployment(componentMeta metav1.ObjectMeta, return deployment } +func GetKServeContainerPort(podSpec *corev1.PodSpec) string { + for _, container := range podSpec.Containers { + if container.Name == "kserve-container" { + if len(container.Ports) > 0 { + return strconv.Itoa(int(container.Ports[0].ContainerPort)) + } + } + } + return "" +} + +func generateOauthProxyContainer(upstreamPort string, namespace string) corev1.Container { + args := []string{ + `--https-address=:8443`, + `--provider=openshift`, + `--openshift-service-account=kserve-sa`, + `--upstream=http://localhost:$upstreamPort`, + `--tls-cert=/etc/tls/private/tls.crt`, + `--tls-key=/etc/tls/private/tls.key`, + `--cookie-secret=SECRET`, + `--openshift-delegate-urls={"/": {"namespace": "$isvcNamespace", "resource": "services", "verb": "get"}}`, + `--openshift-sar={"namespace": "$isvcNamespace", "resource": "services", "verb": "get"}`, + `--skip-auth-regex="(^/metrics|^/apis/v1beta1/healthz)"`, + } + args[3] = strings.ReplaceAll(args[3], "$upstreamPort", upstreamPort) + args[7] = strings.ReplaceAll(args[7], "$isvcNamespace", namespace) + args[8] = strings.ReplaceAll(args[8], "$isvcNamespace", namespace) + return corev1.Container{ + Name: "oauth-proxy", + Args: args, + Image: constants.OauthProxyImage, + Ports: []corev1.ContainerPort{ + { + ContainerPort: constants.OauthProxyPort, + Name: "https", + }, + }, + LivenessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/oauth/healthz", + Port: intstr.FromInt(constants.OauthProxyPort), + Scheme: corev1.URISchemeHTTPS, + }, + }, + InitialDelaySeconds: 30, + TimeoutSeconds: 1, + PeriodSeconds: 5, + SuccessThreshold: 1, + FailureThreshold: 3, + }, + ReadinessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/oauth/healthz", + Port: intstr.FromInt(constants.OauthProxyPort), + Scheme: corev1.URISchemeHTTPS, + }, + }, + InitialDelaySeconds: 5, + TimeoutSeconds: 1, + PeriodSeconds: 5, + SuccessThreshold: 1, + FailureThreshold: 3, + }, + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse(constants.OauthProxyResourceCPULimit), + corev1.ResourceMemory: resource.MustParse(constants.OauthProxyResourceMemoryLimit), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse(constants.OauthProxyResourceCPURequest), + corev1.ResourceMemory: resource.MustParse(constants.OauthProxyResourceMemoryRequest), + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: tlsVolumeName, + MountPath: "/etc/tls/private", + }, + }, + } +} + // checkDeploymentExist checks if the deployment exists? func (r *DeploymentReconciler) checkDeploymentExist(client kclient.Client) (constants.CheckResultType, *appsv1.Deployment, error) { // get deployment @@ -199,6 +308,8 @@ func setDefaultDeploymentSpec(spec *appsv1.DeploymentSpec) { progressDeadlineSeconds := int32(600) spec.ProgressDeadlineSeconds = &progressDeadlineSeconds } + + spec.Template.Spec.ServiceAccountName = constants.KserveServiceAccountName } // Reconcile ... diff --git a/pkg/controller/v1beta1/inferenceservice/reconcilers/ingress/kube_ingress_reconciler.go b/pkg/controller/v1beta1/inferenceservice/reconcilers/ingress/kube_ingress_reconciler.go index f0d508415b..a720318055 100644 --- a/pkg/controller/v1beta1/inferenceservice/reconcilers/ingress/kube_ingress_reconciler.go +++ b/pkg/controller/v1beta1/inferenceservice/reconcilers/ingress/kube_ingress_reconciler.go @@ -150,7 +150,7 @@ func generateIngressHost(ingressConfig *v1beta1.IngressConfig, } func createRawIngress(scheme *runtime.Scheme, isvc *v1beta1.InferenceService, - ingressConfig *v1beta1.IngressConfig, client client.Client) (*netv1.Ingress, error) { + ingressConfig *v1beta1.IngressConfig, client client.Client, port int32) (*netv1.Ingress, error) { if !isvc.Status.IsConditionReady(v1beta1.PredictorReady) { isvc.Status.SetCondition(v1beta1.IngressReady, &apis.Condition{ Type: v1beta1.IngressReady, @@ -193,11 +193,11 @@ func createRawIngress(scheme *runtime.Scheme, isvc *v1beta1.InferenceService, if err != nil { return nil, fmt.Errorf("failed creating explainer ingress host: %w", err) } - rules = append(rules, generateRule(explainerHost, explainerName, "/", constants.CommonDefaultHttpPort)) + rules = append(rules, generateRule(explainerHost, explainerName, "/", port)) } // :predict routes to the transformer when there are both predictor and transformer - rules = append(rules, generateRule(host, transformerName, "/", constants.CommonDefaultHttpPort)) - rules = append(rules, generateRule(transformerHost, predictorName, "/", constants.CommonDefaultHttpPort)) + rules = append(rules, generateRule(host, transformerName, "/", port)) + rules = append(rules, generateRule(transformerHost, predictorName, "/", port)) case isvc.Spec.Explainer != nil: if !isvc.Status.IsConditionReady(v1beta1.ExplainerReady) { isvc.Status.SetCondition(v1beta1.IngressReady, &apis.Condition{ @@ -222,8 +222,8 @@ func createRawIngress(scheme *runtime.Scheme, isvc *v1beta1.InferenceService, return nil, fmt.Errorf("failed creating explainer ingress host: %w", err) } // :predict routes to the predictor when there is only predictor and explainer - rules = append(rules, generateRule(host, predictorName, "/", constants.CommonDefaultHttpPort)) - rules = append(rules, generateRule(explainerHost, explainerName, "/", constants.CommonDefaultHttpPort)) + rules = append(rules, generateRule(host, predictorName, "/", port)) + rules = append(rules, generateRule(explainerHost, explainerName, "/", port)) default: err := client.Get(context.TODO(), types.NamespacedName{Name: constants.DefaultPredictorServiceName(isvc.Name), Namespace: isvc.Namespace}, existing) if err == nil { @@ -233,14 +233,14 @@ func createRawIngress(scheme *runtime.Scheme, isvc *v1beta1.InferenceService, if err != nil { return nil, fmt.Errorf("failed creating top level predictor ingress host: %w", err) } - rules = append(rules, generateRule(host, predictorName, "/", constants.CommonDefaultHttpPort)) + rules = append(rules, generateRule(host, predictorName, "/", port)) } // add predictor rule predictorHost, err := generateIngressHost(ingressConfig, isvc, string(constants.Predictor), false, predictorName) if err != nil { return nil, fmt.Errorf("failed creating predictor ingress host: %w", err) } - rules = append(rules, generateRule(predictorHost, predictorName, "/", constants.CommonDefaultHttpPort)) + rules = append(rules, generateRule(predictorHost, predictorName, "/", port)) ingress := &netv1.Ingress{ ObjectMeta: metav1.ObjectMeta{ @@ -266,6 +266,8 @@ func semanticIngressEquals(desired, existing *netv1.Ingress) bool { func (r *RawIngressReconciler) Reconcile(isvc *v1beta1.InferenceService) error { var err error isInternal := false + hasAuth := false + port := constants.CommonDefaultHttpPort // disable ingress creation if service is labelled with cluster local or kserve domain is cluster local if val, ok := isvc.Labels[constants.NetworkVisibility]; ok && val == constants.ClusterLocalVisibility { isInternal = true @@ -273,14 +275,24 @@ func (r *RawIngressReconciler) Reconcile(isvc *v1beta1.InferenceService) error { if r.ingressConfig.IngressDomain == constants.ClusterLocalDomain { isInternal = true } + if val, ok := isvc.Labels[constants.ODHKserveRawAuth]; ok && val == "true" { + port = constants.OauthProxyPort + hasAuth = true + } if !isInternal && !r.ingressConfig.DisableIngressCreation { - ingress, err := createRawIngress(r.scheme, isvc, r.ingressConfig, r.client) + ingress, err := createRawIngress(r.scheme, isvc, r.ingressConfig, r.client, int32(port)) if ingress == nil { return nil } if err != nil { return err } + if hasAuth { + if ingress.Annotations == nil { + ingress.Annotations = make(map[string]string) + } + ingress.Annotations["route.openshift.io/termination"] = "reencrypt" + } // reconcile ingress existingIngress := &netv1.Ingress{} err = r.client.Get(context.TODO(), types.NamespacedName{ diff --git a/pkg/controller/v1beta1/inferenceservice/reconcilers/raw/raw_kube_reconciler.go b/pkg/controller/v1beta1/inferenceservice/reconcilers/raw/raw_kube_reconciler.go index a849d7f922..a01048e416 100644 --- a/pkg/controller/v1beta1/inferenceservice/reconcilers/raw/raw_kube_reconciler.go +++ b/pkg/controller/v1beta1/inferenceservice/reconcilers/raw/raw_kube_reconciler.go @@ -89,13 +89,14 @@ func createRawURL(clientset kubernetes.Interface, metadata metav1.ObjectMeta) (* // Reconcile ... func (r *RawKubeReconciler) Reconcile() (*appsv1.Deployment, error) { - // reconcile Deployment - deployment, err := r.Deployment.Reconcile() + //reconciling service before deployment because we want to use "service.beta.openshift.io/serving-cert-secret-name" + // reconcile Service + _, err := r.Service.Reconcile() if err != nil { return nil, err } - // reconcile Service - _, err = r.Service.Reconcile() + // reconcile Deployment + deployment, err := r.Deployment.Reconcile() if err != nil { return nil, err } diff --git a/pkg/controller/v1beta1/inferenceservice/reconcilers/service/service_reconciler.go b/pkg/controller/v1beta1/inferenceservice/reconcilers/service/service_reconciler.go index 9ae259e6b2..33e9f67905 100644 --- a/pkg/controller/v1beta1/inferenceservice/reconcilers/service/service_reconciler.go +++ b/pkg/controller/v1beta1/inferenceservice/reconcilers/service/service_reconciler.go @@ -18,6 +18,7 @@ package service import ( "context" + "fmt" "strconv" "github.com/kserve/kserve/pkg/apis/serving/v1beta1" @@ -78,6 +79,9 @@ func createService(componentMeta metav1.ObjectMeta, componentExt *v1beta1.Compon }, Protocol: container.Ports[0].Protocol, } + if servicePort.Name == "" { + servicePort.Name = "http" + } servicePorts = append(servicePorts, servicePort) for i := 1; i < len(container.Ports); i++ { @@ -135,6 +139,26 @@ func createService(componentMeta metav1.ObjectMeta, componentExt *v1beta1.Compon ClusterIP: corev1.ClusterIPNone, }, } + if val, ok := componentMeta.Labels[constants.ODHKserveRawAuth]; ok && val == "true" { + if service.ObjectMeta.Annotations == nil { + service.ObjectMeta.Annotations = make(map[string]string) + } + service.ObjectMeta.Annotations["service.beta.openshift.io/serving-cert-secret-name"] = componentMeta.Name + httpsPort := corev1.ServicePort{ + Name: "https", + Port: constants.OauthProxyPort, + TargetPort: intstr.IntOrString{ + Type: intstr.String, + StrVal: "https", + }, + Protocol: corev1.ProtocolTCP, + } + service.Spec.Ports = append(service.Spec.Ports, httpsPort) + } + + for index, port := range service.Spec.Ports { + fmt.Println(index, port.Name, port.Port, port.TargetPort) + } return service }