diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index 2647f8e5a6b..bccfb86d4ef 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -787,11 +787,25 @@ jobs: kubectl get pods -n kserve kubectl describe pods -n kserve + - name: Log the config map + run: | + kubectl describe configmaps -n kserve inferenceservice-config + - name: Run E2E tests timeout-minutes: 30 run: | ./test/scripts/gh-actions/run-e2e-tests.sh "raw" "6" + - name: Patch inferenceservice config for cluster ip none + run: | + kubectl patch configmaps -n kserve inferenceservice-config --patch-file config/overlays/test/configmap/inferenceservice-enable-cluster-ip.yaml + kubectl describe configmaps -n kserve inferenceservice-config + + - name: Run E2E tests - cluster ip none + timeout-minutes: 30 + run: | + ./test/scripts/gh-actions/run-e2e-tests.sh "rawcipn" "1" + - name: Check system status if: always() run: | diff --git a/.github/workflows/verify-codegen.yml b/.github/workflows/verify-codegen.yml index ff1fd0b1d8d..4845ebb2df6 100644 --- a/.github/workflows/verify-codegen.yml +++ b/.github/workflows/verify-codegen.yml @@ -58,6 +58,6 @@ jobs: for x in $(git diff-index --name-only HEAD -- ./pkg ./python ./charts); do echo "::error file=$x::Please run make generate.%0A$(git diff $x | urlencode)" done - echo "${{ github.repository }} is out of date. Please run make generate" + echo "${{ github.repository }} is out of date. Please run make generate | manifest" exit 1 fi diff --git a/charts/kserve-resources/README.md b/charts/kserve-resources/README.md index 60762bcbf08..cd8e89bee6f 100644 --- a/charts/kserve-resources/README.md +++ b/charts/kserve-resources/README.md @@ -84,6 +84,7 @@ $ helm install kserve oci://ghcr.io/kserve/charts/kserve --version v0.14.0 | kserve.router.image | string | `"kserve/router"` | | | kserve.router.tag | string | `"v0.14.0"` | | | kserve.security.autoMountServiceAccountToken | bool | `true` | | +| kserve.service.serviceClusterIPNone | bool | `false` | | | kserve.servingruntime.art.defaultVersion | string | `"v0.14.0"` | | | kserve.servingruntime.art.image | string | `"kserve/art-explainer"` | | | kserve.servingruntime.art.imagePullSecrets | list | `[]` | | diff --git a/charts/kserve-resources/templates/configmap.yaml b/charts/kserve-resources/templates/configmap.yaml index 07788c91bfc..2bfb9834b69 100644 --- a/charts/kserve-resources/templates/configmap.yaml +++ b/charts/kserve-resources/templates/configmap.yaml @@ -420,6 +420,19 @@ data: "defaultDeploymentMode": "Serverless" } + # ====================================== SERVICE CONFIGURATION ====================================== + # Example + service: |- + { + "serviceClusterIPNone": "false" + } + service: |- + { + # ServiceClusterIPNone is a flag to indicate if the service should have a clusterIP set to None. + # If the DeploymentMode is Raw, the default value for ServiceClusterIPNone if not set is false + # "serviceClusterIPNone": "false" + } + # ====================================== METRICS CONFIGURATION ====================================== # Example metricsAggregator: |- @@ -503,6 +516,10 @@ data: { "defaultDeploymentMode": "{{ .Values.kserve.controller.deploymentMode }}" } + service: |- + { + "serviceClusterIPNone": "{{ .Values.kserve.service.serviceClusterIPNone }}" + } explainers: |- { "art": { diff --git a/charts/kserve-resources/values.yaml b/charts/kserve-resources/values.yaml index bff2b2a5e76..350e9116490 100644 --- a/charts/kserve-resources/values.yaml +++ b/charts/kserve-resources/values.yaml @@ -7,6 +7,8 @@ kserve: router: image: kserve/router tag: *defaultVersion + service: + serviceClusterIPNone: false storage: image: kserve/storage-initializer tag: *defaultVersion diff --git a/cmd/manager/main.go b/cmd/manager/main.go index f9ee225bb78..1ed8530d754 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -50,6 +50,7 @@ import ( v1beta1controller "github.com/kserve/kserve/pkg/controller/v1beta1/inferenceservice" "github.com/kserve/kserve/pkg/webhook/admission/pod" "github.com/kserve/kserve/pkg/webhook/admission/servingruntime" + routev1 "github.com/openshift/api/route/v1" ) var ( @@ -187,7 +188,10 @@ func main() { } } } - + if err = routev1.AddToScheme(mgr.GetScheme()); err != nil { + setupLog.Error(err, "unable to add routev1 APIs to scheme") + os.Exit(1) + } setupLog.Info("Setting up core scheme") if err := v1.AddToScheme(mgr.GetScheme()); err != nil { setupLog.Error(err, "unable to add Core APIs to scheme") diff --git a/config/configmap/inferenceservice.yaml b/config/configmap/inferenceservice.yaml index 5468fc58ce9..3e84dabc9c6 100644 --- a/config/configmap/inferenceservice.yaml +++ b/config/configmap/inferenceservice.yaml @@ -430,7 +430,20 @@ data: # ModelMesh https://kserve.github.io/website/master/admin/modelmesh/ "defaultDeploymentMode": "Serverless" } - + + # ====================================== SERVICE CONFIGURATION ====================================== + # Example + service: |- + { + "serviceClusterIPNone": false + } + service: |- + { + # ServiceClusterIPNone is a boolean flag to indicate if the service should have a clusterIP set to None. + # If the DeploymentMode is Raw, the default value for ServiceClusterIPNone if not set is false + # "serviceClusterIPNone": false + } + # ====================================== METRICS CONFIGURATION ====================================== # Example metricsAggregator: |- @@ -584,3 +597,8 @@ data: { "autoMountServiceAccountToken": true } + + service: |- + { + "serviceClusterIPNone": true + } \ No newline at end of file diff --git a/config/overlays/odh/inferenceservice-config-patch.yaml b/config/overlays/odh/inferenceservice-config-patch.yaml index 9fada7f1c54..1da7fa9fa4b 100644 --- a/config/overlays/odh/inferenceservice-config-patch.yaml +++ b/config/overlays/odh/inferenceservice-config-patch.yaml @@ -5,6 +5,14 @@ metadata: namespace: kserve data: explainers: "{}" + oauthProxy: |- + { + "image" : "$(oauth-proxy)", + "memoryRequest": "64Mi", + "memoryLimit": "128Mi", + "cpuRequest": "100m", + "cpuLimit": "200m" + } storageInitializer: |- { "image" : "$(kserve-storage-initializer)", diff --git a/config/overlays/odh/kustomization.yaml b/config/overlays/odh/kustomization.yaml index 863bc66a360..5ae94a23073 100644 --- a/config/overlays/odh/kustomization.yaml +++ b/config/overlays/odh/kustomization.yaml @@ -54,6 +54,13 @@ vars: apiVersion: v1 kind: ConfigMap name: kserve-parameters +- fieldref: + fieldpath: data.oauth-proxy + name: oauth-proxy + objref: + apiVersion: v1 + kind: ConfigMap + name: kserve-parameters configurations: - params.yaml diff --git a/config/overlays/odh/params.env b/config/overlays/odh/params.env index e4b017513a9..51dbeb1da05 100644 --- a/config/overlays/odh/params.env +++ b/config/overlays/odh/params.env @@ -2,3 +2,4 @@ kserve-controller=quay.io/opendatahub/kserve-controller:v0.14 kserve-agent=quay.io/opendatahub/kserve-agent:v0.14 kserve-router=quay.io/opendatahub/kserve-router:v0.14 kserve-storage-initializer=quay.io/opendatahub/kserve-storage-initializer:v0.14 +oauth-proxy=registry.redhat.io/openshift4/ose-oauth-proxy@sha256:234af927030921ab8f7333f61f967b4b4dee37a1b3cf85689e9e63240dd62800 \ No newline at end of file diff --git a/config/overlays/test/configmap/inferenceservice-enable-cluster-ip.yaml b/config/overlays/test/configmap/inferenceservice-enable-cluster-ip.yaml new file mode 100644 index 00000000000..b877d07a244 --- /dev/null +++ b/config/overlays/test/configmap/inferenceservice-enable-cluster-ip.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: inferenceservice-config + namespace: kserve +data: + service: |- + { + "serviceClusterIPNone": true + } diff --git a/config/overlays/test/configmap/inferenceservice.yaml b/config/overlays/test/configmap/inferenceservice.yaml index 61a919cd730..f1aafac4333 100644 --- a/config/overlays/test/configmap/inferenceservice.yaml +++ b/config/overlays/test/configmap/inferenceservice.yaml @@ -74,4 +74,8 @@ data: "memoryLimit": "500Mi", "cpuRequest": "100m", "cpuLimit": "100m" + } + service: |- + { + "serviceClusterIPNone": false } \ No newline at end of file diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 79e3896b837..7b817d46050 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -111,6 +111,14 @@ rules: - patch - update - watch +- apiGroups: + - route.openshift.io + resources: + - routes + verbs: + - get + - list + - watch - apiGroups: - serving.knative.dev resources: diff --git a/go.mod b/go.mod index 91c1b00b79b..4c32d5e1c55 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/kelseyhightower/envconfig v1.4.0 github.com/onsi/ginkgo/v2 v2.20.1 github.com/onsi/gomega v1.34.2 + github.com/openshift/api v0.0.0-20241108213852-e22f17d9b7f5 github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 @@ -30,10 +31,10 @@ require ( gopkg.in/go-playground/validator.v9 v9.31.0 istio.io/api v1.23.0 istio.io/client-go v1.23.0 - k8s.io/api v0.30.4 - k8s.io/apimachinery v0.30.4 - k8s.io/client-go v0.30.4 - k8s.io/code-generator v0.30.4 + k8s.io/api v0.31.2 + k8s.io/apimachinery v0.31.2 + k8s.io/client-go v0.31.0 + k8s.io/code-generator v0.31.0 k8s.io/component-helpers v0.30.4 k8s.io/klog v1.0.0 k8s.io/kube-openapi v0.0.0-20240827152857-f7e401e7b4c2 @@ -41,7 +42,7 @@ require ( knative.dev/networking v0.0.0-20240815142417-37fdbdd0854b knative.dev/pkg v0.0.0-20240815051656-89743d9bbf7c knative.dev/serving v0.42.2 - sigs.k8s.io/controller-runtime v0.18.5 + sigs.k8s.io/controller-runtime v0.19.1 sigs.k8s.io/yaml v1.4.0 ) @@ -59,9 +60,9 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.12.1 // indirect - github.com/evanphx/json-patch v5.9.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -107,6 +108,7 @@ require ( github.com/prometheus/statsd_exporter v0.27.1 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect + github.com/x448/float16 v0.8.4 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.54.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect @@ -115,31 +117,29 @@ require ( go.opentelemetry.io/otel/trace v1.29.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.31.0 // indirect + golang.org/x/crypto v0.26.0 // indirect golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 // indirect golang.org/x/mod v0.20.0 // indirect golang.org/x/net v0.28.0 // indirect golang.org/x/oauth2 v0.22.0 // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.28.0 // indirect - golang.org/x/term v0.27.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/term v0.23.0 // indirect + golang.org/x/text v0.17.0 // indirect golang.org/x/time v0.6.0 // indirect golang.org/x/tools v0.24.0 // indirect google.golang.org/genproto v0.0.0-20240827150818-7e3bb234dfed // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240827150818-7e3bb234dfed // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240827150818-7e3bb234dfed // indirect google.golang.org/grpc v1.66.0 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/go-playground/assert.v1 v1.2.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiextensions-apiserver v0.30.4 // indirect + k8s.io/apiextensions-apiserver v0.31.0 // indirect k8s.io/gengo/v2 v2.0.0-20240826214909-a7b603a56eb7 // indirect k8s.io/klog/v2 v2.130.1 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect ) - -// Fixes CVE-2024-45338 -replace golang.org/x/net => golang.org/x/net v0.33.0 diff --git a/go.sum b/go.sum index 3e80ee76029..d4b08833c45 100644 --- a/go.sum +++ b/go.sum @@ -100,6 +100,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/getkin/kin-openapi v0.127.0 h1:Mghqi3Dhryf3F8vR370nN67pAERW+3a95vomb3MAREY= github.com/getkin/kin-openapi v0.127.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -304,6 +306,8 @@ github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/openshift/api v0.0.0-20241108213852-e22f17d9b7f5 h1:GJXiIZyRkQs4b++xfkz58m8T0ExcWf6PYJDZIeJFj7s= +github.com/openshift/api v0.0.0-20241108213852-e22f17d9b7f5/go.mod h1:Shkl4HanLwDiiBzakv+con/aMGnVE2MAGvoKp5oyYUo= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -384,11 +388,12 @@ github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0 github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -421,12 +426,13 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -459,14 +465,44 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -487,12 +523,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -515,6 +547,7 @@ golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -523,31 +556,31 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220708085239-5a0f0661e09d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -595,9 +628,6 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -706,6 +736,8 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/validator.v9 v9.31.0 h1:bmXmP2RSNtFES+bn4uYuHT7iJFJv7Vj+an+ZQdDaD1M= @@ -735,16 +767,16 @@ istio.io/api v1.23.0 h1:yqv3lNW6XSYS5XkbEkxsmFROXIQznp4lFWqj7xKEqCA= istio.io/api v1.23.0/go.mod h1:QPSTGXuIQdnZFEm3myf9NZ5uBMwCdJWUvfj9ZZ+2oBM= istio.io/client-go v1.23.0 h1://xojbifr84q29WE3eMx74p36hD4lvcejX1KxE3iJvY= istio.io/client-go v1.23.0/go.mod h1:3qX/KBS5aR47QV4JhphcZl5ysnZ53x78TBjNQLM2TC4= -k8s.io/api v0.30.4 h1:XASIELmW8w8q0i1Y4124LqPoWMycLjyQti/fdYHYjCs= -k8s.io/api v0.30.4/go.mod h1:ZqniWRKu7WIeLijbbzetF4U9qZ03cg5IRwl8YVs8mX0= -k8s.io/apiextensions-apiserver v0.30.4 h1:FwOMIk/rzZvM/Gx0IOz0+biZ+dlnlCeyfXW17uzV1qE= -k8s.io/apiextensions-apiserver v0.30.4/go.mod h1:m8cAkJ9PVU8Olb4cPW4hrUDBZGvoSJ0kY0G0CfdGQac= -k8s.io/apimachinery v0.30.4 h1:5QHQI2tInzr8LsT4kU/2+fSeibH1eIHswNx480cqIoY= -k8s.io/apimachinery v0.30.4/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= -k8s.io/client-go v0.30.4 h1:eculUe+HPQoPbixfwmaSZGsKcOf7D288tH6hDAdd+wY= -k8s.io/client-go v0.30.4/go.mod h1:IBS0R/Mt0LHkNHF4E6n+SUDPG7+m2po6RZU7YHeOpzc= -k8s.io/code-generator v0.30.4 h1:1J2AcpPNBGh/NH9+m4TDh8Yj+mSbM+JyQhH0QdIMwmE= -k8s.io/code-generator v0.30.4/go.mod h1:Dd8gxOr5ieh9yHCLKnIkKDmk1H2glH8nYCAqwFogD2M= +k8s.io/api v0.31.2 h1:3wLBbL5Uom/8Zy98GRPXpJ254nEFpl+hwndmk9RwmL0= +k8s.io/api v0.31.2/go.mod h1:bWmGvrGPssSK1ljmLzd3pwCQ9MgoTsRCuK35u6SygUk= +k8s.io/apiextensions-apiserver v0.31.0 h1:fZgCVhGwsclj3qCw1buVXCV6khjRzKC5eCFt24kyLSk= +k8s.io/apiextensions-apiserver v0.31.0/go.mod h1:b9aMDEYaEe5sdK+1T0KU78ApR/5ZVp4i56VacZYEHxk= +k8s.io/apimachinery v0.31.2 h1:i4vUt2hPK56W6mlT7Ry+AO8eEsyxMD1U44NR22CLTYw= +k8s.io/apimachinery v0.31.2/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/client-go v0.31.0 h1:QqEJzNjbN2Yv1H79SsS+SWnXkBgVu4Pj3CJQgbx0gI8= +k8s.io/client-go v0.31.0/go.mod h1:Y9wvC76g4fLjmU0BA+rV+h2cncoadjvjjkkIGoTLcGU= +k8s.io/code-generator v0.31.0 h1:w607nrMi1KeDKB3/F/J4lIoOgAwc+gV9ZKew4XRfMp8= +k8s.io/code-generator v0.31.0/go.mod h1:84y4w3es8rOJOUUP1rLsIiGlO1JuEaPFXQPA9e/K6U0= k8s.io/component-helpers v0.30.4 h1:A4KYmrz12HZtGZ8TAnanl0SUx7n6tKduxzB3NHvinr0= k8s.io/component-helpers v0.30.4/go.mod h1:h5D4gI8hGQXMHw90qJq41PRUJrn2dvFA3ElZFUTzRps= k8s.io/gengo/v2 v2.0.0-20240826214909-a7b603a56eb7 h1:cErOOTkQ3JW19o4lo91fFurouhP8NcoBvb7CkvhZZpk= @@ -766,8 +798,8 @@ knative.dev/serving v0.42.2/go.mod h1:3cgU8/864RcqA0ZPrc3jFcmS3uJL/mOlUZiYsXonwa rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/controller-runtime v0.18.5 h1:nTHio/W+Q4aBlQMgbnC5hZb4IjIidyrizMai9P6n4Rk= -sigs.k8s.io/controller-runtime v0.18.5/go.mod h1:TVoGrfdpbA9VRFaRnKgk9P5/atA0pMwq+f+msb9M8Sg= +sigs.k8s.io/controller-runtime v0.19.1 h1:Son+Q40+Be3QWb+niBXAg2vFiYWolDjjRfO8hn/cxOk= +sigs.k8s.io/controller-runtime v0.19.1/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= diff --git a/hack/update-codegen.sh b/hack/update-codegen.sh index bdfa2bae454..03c4e83b547 100755 --- a/hack/update-codegen.sh +++ b/hack/update-codegen.sh @@ -22,6 +22,9 @@ SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")" SCRIPT_ROOT="${SCRIPT_DIR}/.." CODEGEN_VERSION=$(cd "${SCRIPT_ROOT}" && grep 'k8s.io/code-generator' go.mod | awk '{print $2}') +# For debugging purposes +echo "Codegen version ${CODEGEN_VERSION}" + if [ -z "${GOPATH:-}" ]; then GOPATH=$(go env GOPATH) export GOPATH diff --git a/pkg/apis/serving/v1beta1/configmap.go b/pkg/apis/serving/v1beta1/configmap.go index 27d7770bf33..f5461ccbb46 100644 --- a/pkg/apis/serving/v1beta1/configmap.go +++ b/pkg/apis/serving/v1beta1/configmap.go @@ -36,6 +36,7 @@ const ( DeployConfigName = "deploy" LocalModelConfigName = "localModel" SecurityConfigName = "security" + ServiceConfigName = "service" ) const ( @@ -79,6 +80,15 @@ type IngressConfig struct { DisableIngressCreation bool `json:"disableIngressCreation,omitempty"` } +// +kubebuilder:object:generate=false +type OauthConfig struct { + Image string `json:"image"` + CpuLimit string `json:"cpuLimit"` + CpuRequest string `json:"cpuRequest"` + MemoryLimit string `json:"memoryLimit"` + MemoryRequest string `json:"memoryRequest"` +} + // +kubebuilder:object:generate=false type DeployConfig struct { DefaultDeploymentMode string `json:"defaultDeploymentMode,omitempty"` @@ -97,6 +107,13 @@ type SecurityConfig struct { AutoMountServiceAccountToken bool `json:"autoMountServiceAccountToken"` } +// +kubebuilder:object:generate=false +type ServiceConfig struct { + // ServiceClusterIPNone is a boolean flag to indicate if the service should have a clusterIP set to None. + // If the DeploymentMode is Raw, the default value for ServiceClusterIPNone is false when the value is absent. + ServiceClusterIPNone bool `json:"serviceClusterIPNone,omitempty"` +} + func NewInferenceServicesConfig(clientset kubernetes.Interface) (*InferenceServicesConfig, error) { configMap, err := clientset.CoreV1().ConfigMaps(constants.KServeNamespace).Get(context.TODO(), constants.InferenceServiceConfigMapName, metav1.GetOptions{}) if err != nil { @@ -227,3 +244,19 @@ func NewSecurityConfig(clientset kubernetes.Interface) (*SecurityConfig, error) } return securityConfig, nil } + +func NewServiceConfig(clientset kubernetes.Interface) (*ServiceConfig, error) { + configMap, err := clientset.CoreV1().ConfigMaps(constants.KServeNamespace).Get(context.TODO(), constants.InferenceServiceConfigMapName, metav1.GetOptions{}) + + if err != nil { + return nil, err + } + serviceConfig := &ServiceConfig{} + if service, ok := configMap.Data[ServiceConfigName]; ok { + err := json.Unmarshal([]byte(service), &serviceConfig) + if err != nil { + return nil, fmt.Errorf("unable to parse service config json: %w", err) + } + } + return serviceConfig, nil +} diff --git a/pkg/apis/serving/v1beta1/configmap_test.go b/pkg/apis/serving/v1beta1/configmap_test.go index b5ee02da061..71e1e53568d 100644 --- a/pkg/apis/serving/v1beta1/configmap_test.go +++ b/pkg/apis/serving/v1beta1/configmap_test.go @@ -46,6 +46,9 @@ var ( "additionalIngressDomains": ["%s","%s"] }`, KnativeIngressGateway, KnativeLocalGatewayService, KnativeLocalGateway, LocalGatewayService, IngressDomain, AdditionalDomain, AdditionalDomainExtra) + ServiceConfigData = fmt.Sprintf(`{ + "serviceClusterIPNone" : %t + }`, true) ) func TestNewInferenceServiceConfig(t *testing.T) { @@ -110,3 +113,39 @@ func TestNewDeployConfig(t *testing.T) { g.Expect(err).Should(gomega.BeNil()) g.Expect(deployConfig).ShouldNot(gomega.BeNil()) } + +func TestNewServiceConfig(t *testing.T) { + g := gomega.NewGomegaWithT(t) + // nothing declared + empty := fakeclientset.NewSimpleClientset(&v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: constants.InferenceServiceConfigMapName, Namespace: constants.KServeNamespace}, + }) + emp, err := NewServiceConfig(empty) + g.Expect(err).Should(gomega.BeNil()) + g.Expect(emp).ShouldNot(gomega.BeNil()) + + // with value + withTrue := fakeclientset.NewSimpleClientset(&v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: constants.InferenceServiceConfigMapName, Namespace: constants.KServeNamespace}, + Data: map[string]string{ + ServiceConfigName: ServiceConfigData, + }, + }) + wt, err := NewServiceConfig(withTrue) + g.Expect(err).Should(gomega.BeNil()) + g.Expect(wt).ShouldNot(gomega.BeNil()) + g.Expect(wt.ServiceClusterIPNone).Should(gomega.BeTrue()) + + // no value, should be nil + noValue := fakeclientset.NewSimpleClientset(&v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: constants.InferenceServiceConfigMapName, Namespace: constants.KServeNamespace}, + Data: map[string]string{ + ServiceConfigName: `{}`, + }, + }) + nv, err := NewServiceConfig(noValue) + g.Expect(err).Should(gomega.BeNil()) + g.Expect(nv).ShouldNot(gomega.BeNil()) + g.Expect(nv.ServiceClusterIPNone).Should(gomega.BeFalse()) + +} diff --git a/pkg/apis/serving/v1beta1/inference_service_status.go b/pkg/apis/serving/v1beta1/inference_service_status.go index cce0812b795..438c6490b95 100644 --- a/pkg/apis/serving/v1beta1/inference_service_status.go +++ b/pkg/apis/serving/v1beta1/inference_service_status.go @@ -336,9 +336,12 @@ func (ss *InferenceServiceStatus) PropagateRawStatus( } condition := getDeploymentCondition(deploymentList, appsv1.DeploymentAvailable) - if condition != nil && condition.Status == v1.ConditionTrue { - statusSpec.URL = url - } + // currently the component url is disabled as this url generated based on ingressConfig. This is incompatible with + // rawdeployment changes as the url depends on the route creation, if a route is requested. + // TODO: add back component url deterministicly + // if condition != nil && condition.Status == v1.ConditionTrue { + // statusSpec.URL = url + //} readyCondition := readyConditionsMap[component] ss.SetCondition(readyCondition, condition) ss.Components[component] = statusSpec diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 98952f3d700..b66debabe43 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -130,11 +130,14 @@ var ( // kserve networking constants const ( - NetworkVisibility = "networking.kserve.io/visibility" - ClusterLocalVisibility = "cluster-local" - ClusterLocalDomain = "svc.cluster.local" - IsvcNameHeader = "KServe-Isvc-Name" - IsvcNamespaceHeader = "KServe-Isvc-Namespace" + NetworkVisibility = "networking.kserve.io/visibility" + ClusterLocalVisibility = "cluster-local" + ClusterLocalDomain = "svc.cluster.local" + IsvcNameHeader = "KServe-Isvc-Name" + IsvcNamespaceHeader = "KServe-Isvc-Namespace" + ODHKserveRawAuth = "security.opendatahub.io/enable-auth" + ODHRouteEnabled = "exposed" + ServingCertSecretSuffix = "-serving-cert" ) // StorageSpec Constants @@ -450,6 +453,17 @@ const ( SupportedModelMLFlow = "mlflow" ) +// opendatahub rawDeployment Auth +const ( + OauthProxyPort = 8443 + OauthProxyResourceMemoryLimit = "128Mi" + OauthProxyResourceCPULimit = "200m" + OauthProxyResourceMemoryRequest = "64Mi" + OauthProxyResourceCPURequest = "100m" + OauthProxyImage = "registry.redhat.io/openshift4/ose-oauth-proxy@sha256:234af927030921ab8f7333f61f967b4b4dee37a1b3cf85689e9e63240dd62800" + DefaultServiceAccount = "default" +) + type ProtocolVersion int const ( diff --git a/pkg/controller/v1beta1/inferenceservice/controller.go b/pkg/controller/v1beta1/inferenceservice/controller.go index c8ca033344b..7b01742acb8 100644 --- a/pkg/controller/v1beta1/inferenceservice/controller.go +++ b/pkg/controller/v1beta1/inferenceservice/controller.go @@ -76,6 +76,7 @@ import ( // +kubebuilder:rbac:groups=core,resources=namespaces,verbs=get;list;watch // +kubebuilder:rbac:groups=core,resources=events,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch +// +kubebuilder:rbac:groups=route.openshift.io,resources=routes,verbs=get;list;watch // InferenceServiceState describes the Readiness of the InferenceService type InferenceServiceState string diff --git a/pkg/controller/v1beta1/inferenceservice/rawkube_controller_test.go b/pkg/controller/v1beta1/inferenceservice/rawkube_controller_test.go index 93c43b84d49..63927a83baa 100644 --- a/pkg/controller/v1beta1/inferenceservice/rawkube_controller_test.go +++ b/pkg/controller/v1beta1/inferenceservice/rawkube_controller_test.go @@ -19,6 +19,7 @@ package inferenceservice import ( "context" "fmt" + "time" "github.com/kserve/kserve/pkg/apis/serving/v1alpha1" @@ -40,6 +41,8 @@ import ( autoscalingv2 "k8s.io/api/autoscaling/v2" v1 "k8s.io/api/core/v1" + v1beta1utils "github.com/kserve/kserve/pkg/controller/v1beta1/inferenceservice/utils" + routev1 "github.com/openshift/api/route/v1" netv1 "k8s.io/api/networking/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -414,7 +417,7 @@ var _ = Describe("v1beta1 inference service controller", func() { }, URL: &apis.URL{ Scheme: "http", - Host: "raw-foo-default.example.com", + Host: fmt.Sprintf("%s-predictor.%s.svc.cluster.local", serviceKey.Name, serviceKey.Namespace), }, Address: &duckv1.Addressable{ URL: &apis.URL{ @@ -425,10 +428,10 @@ var _ = Describe("v1beta1 inference service controller", func() { Components: map[v1beta1.ComponentType]v1beta1.ComponentStatusSpec{ v1beta1.PredictorComponent: { LatestCreatedRevision: "", - URL: &apis.URL{ - Scheme: "http", - Host: "raw-foo-predictor-default.example.com", - }, + //URL: &apis.URL{ + // Scheme: "http", + // Host: "raw-foo-predictor-default.example.com", + //}, }, }, ModelStatus: v1beta1.ModelStatus{ @@ -829,7 +832,7 @@ var _ = Describe("v1beta1 inference service controller", func() { }, URL: &apis.URL{ Scheme: "http", - Host: "raw-foo-customized-default.example.com", + Host: fmt.Sprintf("%s-predictor.%s.svc.cluster.local", serviceKey.Name, serviceKey.Namespace), }, Address: &duckv1.Addressable{ URL: &apis.URL{ @@ -840,10 +843,10 @@ var _ = Describe("v1beta1 inference service controller", func() { Components: map[v1beta1.ComponentType]v1beta1.ComponentStatusSpec{ v1beta1.PredictorComponent: { LatestCreatedRevision: "", - URL: &apis.URL{ - Scheme: "http", - Host: "raw-foo-customized-predictor-default.example.com", - }, + //URL: &apis.URL{ + // Scheme: "http", + // Host: "raw-foo-customized-predictor-default.example.com", + //}, }, }, ModelStatus: v1beta1.ModelStatus{ @@ -1235,7 +1238,7 @@ var _ = Describe("v1beta1 inference service controller", func() { }, URL: &apis.URL{ Scheme: "http", - Host: "raw-foo-2-default.example.com", + Host: fmt.Sprintf("%s-predictor.%s.svc.cluster.local", serviceKey.Name, serviceKey.Namespace), }, Address: &duckv1.Addressable{ URL: &apis.URL{ @@ -1246,10 +1249,10 @@ var _ = Describe("v1beta1 inference service controller", func() { Components: map[v1beta1.ComponentType]v1beta1.ComponentStatusSpec{ v1beta1.PredictorComponent: { LatestCreatedRevision: "", - URL: &apis.URL{ - Scheme: "http", - Host: "raw-foo-2-predictor-default.example.com", - }, + //URL: &apis.URL{ + // Scheme: "http", + // Host: "raw-foo-2-predictor-default.example.com", + //}, }, }, ModelStatus: v1beta1.ModelStatus{ @@ -1272,6 +1275,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, &v1beta1.SecurityConfig{AutoMountServiceAccountToken: false}, 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} + Consistently(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{ @@ -1609,7 +1715,7 @@ var _ = Describe("v1beta1 inference service controller", func() { }, URL: &apis.URL{ Scheme: "http", - Host: fmt.Sprintf("%s-default.example.com", serviceName), + Host: fmt.Sprintf("%s-predictor.%s.svc.cluster.local", serviceKey.Name, serviceKey.Namespace), }, Address: &duckv1.Addressable{ URL: &apis.URL{ @@ -1620,10 +1726,10 @@ var _ = Describe("v1beta1 inference service controller", func() { Components: map[v1beta1.ComponentType]v1beta1.ComponentStatusSpec{ v1beta1.PredictorComponent: { LatestCreatedRevision: "", - URL: &apis.URL{ - Scheme: "http", - Host: fmt.Sprintf("%s-predictor-default.example.com", serviceName), - }, + //URL: &apis.URL{ + // Scheme: "http", + // Host: fmt.Sprintf("%s-predictor-default.example.com", serviceName), + //}, }, }, ModelStatus: v1beta1.ModelStatus{ @@ -2042,7 +2148,7 @@ var _ = Describe("v1beta1 inference service controller", func() { }, URL: &apis.URL{ Scheme: "http", - Host: fmt.Sprintf("%s.%s.%s", serviceName, serviceKey.Namespace, domain), + Host: fmt.Sprintf("%s-predictor.%s.svc.cluster.local", serviceKey.Name, serviceKey.Namespace), }, Address: &duckv1.Addressable{ URL: &apis.URL{ @@ -2053,10 +2159,10 @@ var _ = Describe("v1beta1 inference service controller", func() { Components: map[v1beta1.ComponentType]v1beta1.ComponentStatusSpec{ v1beta1.PredictorComponent: { LatestCreatedRevision: "", - URL: &apis.URL{ - Scheme: "http", - Host: fmt.Sprintf("%s-predictor.%s.%s", serviceName, serviceKey.Namespace, domain), - }, + //URL: &apis.URL{ + // Scheme: "http", + // Host: fmt.Sprintf("%s-predictor.%s.%s", serviceName, serviceKey.Namespace, domain), + //}, }, }, ModelStatus: v1beta1.ModelStatus{ @@ -2138,6 +2244,445 @@ 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{ + "oauthProxy": `{"image": "registry.redhat.io/openshift4/ose-oauth-proxy@sha256:234af927030921ab8f7333f61f967b4b4dee37a1b3cf85689e9e63240dd62800", "memoryRequest": "64Mi", "memoryLimit": "128Mi", "cpuRequest": "100m", "cpuLimit": "200m"}`, + "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{ + constants.ODHKserveRawAuth: "true", + constants.NetworkVisibility: constants.ODHRouteEnabled, + }, + }, + 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, &v1beta1.SecurityConfig{AutoMountServiceAccountToken: false}, 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, + constants.ODHKserveRawAuth: "true", + constants.NetworkVisibility: constants.ODHRouteEnabled, + }, + 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 + constants.ServingCertSecretSuffix, + }, + }, + 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`, + `--skip-provider-button`, + `--openshift-service-account=default`, + `--upstream=http://localhost:8080`, + `--tls-cert=/etc/tls/private/tls.crt`, + `--tls-key=/etc/tls/private/tls.key`, + // omit cookie secret arg in unit test as it is generated randomly + //`--cookie-secret=SECRET`, + `--openshift-delegate-urls={"/": {"namespace": "` + serviceKey.Namespace + `", "resource": "inferenceservices", "group": "serving.kserve.io", "name": "` + serviceName + `", "verb": "get"}}`, + `--openshift-sar={"namespace": "` + serviceKey.Namespace + `", "resource": "inferenceservices", "group": "serving.kserve.io", "name": "` + serviceName + `", "verb": "get"}`, + }, + 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 + constants.ServingCertSecretSuffix, + DefaultMode: func(i int32) *int32 { return &i }(420), + }, + }, + }, + }, + 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, + }, + AutomountServiceAccountToken: proto.Bool(false), + }, + }, + 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, + }, + } + // remove the cookie-secret arg from the generated deployment for comparison + cleanedDep := actualDeployment.DeepCopy() + actualDep := v1beta1utils.RemoveCookieSecretArg(*cleanedDep) + Expect(actualDep.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: "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)) + + route := &routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Name: serviceKey.Name, + Namespace: serviceKey.Namespace, + Labels: map[string]string{ + "inferenceservice-name": serviceName, + }, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "serving.kserve.io/v1beta1", + Kind: "InferenceService", + Name: serviceKey.Name, + UID: isvc.GetUID(), + Controller: proto.Bool(true), + BlockOwnerDeletion: proto.Bool(true), + }, + }, + }, + Spec: routev1.RouteSpec{ + Host: "raw-auth-default.example.com", + To: routev1.RouteTargetReference{ + Kind: "Service", + Name: predictorServiceKey.Name, + Weight: proto.Int32(100), + }, + Port: &routev1.RoutePort{ + TargetPort: intstr.FromInt(8443), + }, + TLS: &routev1.TLSConfig{ + Termination: routev1.TLSTerminationReencrypt, + InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyRedirect, + }, + WildcardPolicy: routev1.WildcardPolicyNone, + }, + Status: routev1.RouteStatus{ + Ingress: []routev1.RouteIngress{ + { + Host: "raw-auth-default.example.com", + Conditions: []routev1.RouteIngressCondition{ + { + Type: routev1.RouteAdmitted, + Status: v1.ConditionTrue, + }, + }, + }, + }, + }, + } + Expect(k8sClient.Create(context.TODO(), route)).Should(Succeed()) + + //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()) + + // 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: "https", + Host: "raw-auth-default.example.com", + }, + Address: &duckv1.Addressable{ + URL: &apis.URL{ + Scheme: "https", + Host: fmt.Sprintf("%s-predictor.%s.svc.cluster.local:8443", 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()) + + }) + }) Context("When creating inference service with raw kube predictor with workerSpec", func() { var ( ctx context.Context diff --git a/pkg/controller/v1beta1/inferenceservice/reconcilers/deployment/deployment_reconciler.go b/pkg/controller/v1beta1/inferenceservice/reconcilers/deployment/deployment_reconciler.go index 4433c3998df..cc43124ca9a 100644 --- a/pkg/controller/v1beta1/inferenceservice/reconcilers/deployment/deployment_reconciler.go +++ b/pkg/controller/v1beta1/inferenceservice/reconcilers/deployment/deployment_reconciler.go @@ -18,26 +18,30 @@ package deployment import ( "context" + "crypto/rand" + "encoding/base64" "encoding/json" "fmt" "strconv" "strings" + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/client-go/kubernetes" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/kserve/kserve/pkg/apis/serving/v1beta1" "github.com/kserve/kserve/pkg/constants" + v1beta1utils "github.com/kserve/kserve/pkg/controller/v1beta1/inferenceservice/utils" "github.com/kserve/kserve/pkg/utils" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" apierr "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/util/strategicpatch" "knative.dev/pkg/kmp" - "sigs.k8s.io/controller-runtime/pkg/client" kclient "sigs.k8s.io/controller-runtime/pkg/client" logf "sigs.k8s.io/controller-runtime/pkg/log" ) @@ -52,22 +56,51 @@ type DeploymentReconciler struct { componentExt *v1beta1.ComponentExtensionSpec } +const ( + tlsVolumeName = "proxy-tls" + oauthProxy = "oauthProxy" +) + func NewDeploymentReconciler(client kclient.Client, + clientset kubernetes.Interface, scheme *runtime.Scheme, componentMeta metav1.ObjectMeta, workerComponentMeta metav1.ObjectMeta, componentExt *v1beta1.ComponentExtensionSpec, - podSpec *corev1.PodSpec, workerPodSpec *corev1.PodSpec) *DeploymentReconciler { + podSpec *corev1.PodSpec, workerPodSpec *corev1.PodSpec) (*DeploymentReconciler, error) { + deploymentList, err := createRawDeploymentODH(clientset, componentMeta, workerComponentMeta, componentExt, podSpec, workerPodSpec) + if err != nil { + return nil, err + } return &DeploymentReconciler{ client: client, scheme: scheme, - DeploymentList: createRawDeployment(componentMeta, workerComponentMeta, componentExt, podSpec, workerPodSpec), + DeploymentList: deploymentList, componentExt: componentExt, + }, nil +} + +func createRawDeploymentODH(clientset kubernetes.Interface, componentMeta metav1.ObjectMeta, workerComponentMeta metav1.ObjectMeta, + componentExt *v1beta1.ComponentExtensionSpec, + podSpec *corev1.PodSpec, workerPodSpec *corev1.PodSpec) ([]*appsv1.Deployment, error) { + deploymentList, err := createRawDeployment(componentMeta, workerComponentMeta, componentExt, podSpec, workerPodSpec) + if err != nil { + return nil, err } + if val, ok := componentMeta.Labels[constants.ODHKserveRawAuth]; ok && val == "true" { + for _, deployment := range deploymentList { + err := addOauthContainerToDeployment(clientset, deployment, componentMeta, componentExt, podSpec) + if err != nil { + return nil, err + } + } + } + return deploymentList, nil } + func createRawDeployment(componentMeta metav1.ObjectMeta, workerComponentMeta metav1.ObjectMeta, componentExt *v1beta1.ComponentExtensionSpec, - podSpec *corev1.PodSpec, workerPodSpec *corev1.PodSpec) []*appsv1.Deployment { + podSpec *corev1.PodSpec, workerPodSpec *corev1.PodSpec) ([]*appsv1.Deployment, error) { var deploymentList []*appsv1.Deployment var workerNodeReplicas int32 var tensorParallelSize string @@ -93,7 +126,10 @@ func createRawDeployment(componentMeta metav1.ObjectMeta, workerComponentMeta me } } - defaultDeployment := createRawDefaultDeployment(componentMeta, componentExt, podSpec) + defaultDeployment, err := createRawDefaultDeployment(componentMeta, componentExt, podSpec) + if err != nil { + return nil, err + } if multiNodeEnabled { // Use defaut value(1) if tensor-parallel-size is not set (gpu count) tensorParallelSize = constants.DefaultTensorParallelSize @@ -120,16 +156,16 @@ func createRawDeployment(componentMeta metav1.ObjectMeta, workerComponentMeta me addGPUResourceToDeployment(workerDeployment, constants.WorkerContainerName, tensorParallelSize) deploymentList = append(deploymentList, workerDeployment) } - - return deploymentList + return deploymentList, nil } func createRawDefaultDeployment(componentMeta metav1.ObjectMeta, componentExt *v1beta1.ComponentExtensionSpec, - podSpec *corev1.PodSpec) *appsv1.Deployment { + podSpec *corev1.PodSpec) (*appsv1.Deployment, error) { podMetadata := componentMeta podMetadata.Labels["app"] = constants.GetRawServiceLabel(componentMeta.Name) setDefaultPodSpec(podSpec) + deployment := &appsv1.Deployment{ ObjectMeta: componentMeta, Spec: appsv1.DeploymentSpec{ @@ -148,8 +184,60 @@ func createRawDefaultDeployment(componentMeta metav1.ObjectMeta, deployment.Spec.Strategy = *componentExt.DeploymentStrategy } setDefaultDeploymentSpec(&deployment.Spec) - return deployment + return deployment, nil +} + +func addOauthContainerToDeployment(clientset kubernetes.Interface, deployment *appsv1.Deployment, componentMeta metav1.ObjectMeta, componentExt *v1beta1.ComponentExtensionSpec, + podSpec *corev1.PodSpec) error { + var isvcname string + var upstreamPort string + var sa string + if val, ok := componentMeta.Labels[constants.InferenceServiceLabel]; ok { + isvcname = val + } else { + isvcname = componentMeta.Name + } + if val, ok := componentMeta.Labels[constants.ODHKserveRawAuth]; ok && val == "true" { + switch { + case componentExt != nil && componentExt.Batcher != nil: + upstreamPort = constants.InferenceServiceDefaultAgentPortStr + case componentExt != nil && componentExt.Logger != nil: + upstreamPort = constants.InferenceServiceDefaultAgentPortStr + default: + upstreamPort = GetKServeContainerPort(podSpec) + if upstreamPort == "" { + upstreamPort = constants.InferenceServiceDefaultHttpPort + } + } + if podSpec.ServiceAccountName == "" { + sa = constants.DefaultServiceAccount + } else { + sa = podSpec.ServiceAccountName + } + oauthProxyContainer, err := generateOauthProxyContainer(clientset, isvcname, componentMeta.Namespace, upstreamPort, sa) + if err != nil { + // return the deployment without the oauth proxy container if there was an error + // This is required for the deployment_reconciler_tests + return err + } + updatedPodSpec := deployment.Spec.Template.Spec.DeepCopy() + // updatedPodSpec := podSpec.DeepCopy() + updatedPodSpec.Containers = append(updatedPodSpec.Containers, *oauthProxyContainer) + tlsSecretVolume := corev1.Volume{ + Name: tlsVolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: componentMeta.Name + constants.ServingCertSecretSuffix, + DefaultMode: func(i int32) *int32 { return &i }(420), + }, + }, + } + updatedPodSpec.Volumes = append(updatedPodSpec.Volumes, tlsSecretVolume) + deployment.Spec.Template.Spec = *updatedPodSpec + } + return nil } + func createRawWorkerDeployment(componentMeta metav1.ObjectMeta, componentExt *v1beta1.ComponentExtensionSpec, podSpec *corev1.PodSpec, predictorName string, replicas int32) *appsv1.Deployment { @@ -180,6 +268,127 @@ func createRawWorkerDeployment(componentMeta metav1.ObjectMeta, return deployment } +func GetKServeContainerPort(podSpec *corev1.PodSpec) string { + var kserveContainerPort string + + for _, container := range podSpec.Containers { + if container.Name == "transformer-container" { + if len(container.Ports) > 0 { + return strconv.Itoa(int(container.Ports[0].ContainerPort)) + } + } + if container.Name == "kserve-container" { + if len(container.Ports) > 0 { + kserveContainerPort = strconv.Itoa(int(container.Ports[0].ContainerPort)) + } + } + } + + return kserveContainerPort +} + +func generateOauthProxyContainer(clientset kubernetes.Interface, isvc string, namespace string, upstreamPort string, sa string) (*corev1.Container, error) { + isvcConfigMap, err := clientset.CoreV1().ConfigMaps(constants.KServeNamespace).Get(context.TODO(), constants.InferenceServiceConfigMapName, metav1.GetOptions{}) + if err != nil { + return nil, err + } + oauthProxyJSON := strings.TrimSpace(isvcConfigMap.Data["oauthProxy"]) + oauthProxyConfig := v1beta1.OauthConfig{} + if err := json.Unmarshal([]byte(oauthProxyJSON), &oauthProxyConfig); err != nil { + return nil, err + } + if oauthProxyConfig.Image == "" || oauthProxyConfig.MemoryRequest == "" || oauthProxyConfig.MemoryLimit == "" || + oauthProxyConfig.CpuRequest == "" || oauthProxyConfig.CpuLimit == "" { + return nil, fmt.Errorf("one or more oauthProxyConfig fields are empty") + } + oauthImage := oauthProxyConfig.Image + oauthMemoryRequest := oauthProxyConfig.MemoryRequest + oauthMemoryLimit := oauthProxyConfig.MemoryLimit + oauthCpuRequest := oauthProxyConfig.CpuRequest + oauthCpuLimit := oauthProxyConfig.CpuLimit + + cookieSecret, err := generateCookieSecret() + if err != nil { + return nil, err + } + + return &corev1.Container{ + Name: "oauth-proxy", + Args: []string{ + `--https-address=:` + strconv.Itoa(constants.OauthProxyPort), + `--provider=openshift`, + `--skip-provider-button`, + `--openshift-service-account=` + sa, + `--upstream=http://localhost:` + upstreamPort, + `--tls-cert=/etc/tls/private/tls.crt`, + `--tls-key=/etc/tls/private/tls.key`, + `--cookie-secret=` + cookieSecret, + `--openshift-delegate-urls={"/": {"namespace": "` + namespace + `", "resource": "inferenceservices", "group": "serving.kserve.io", "name": "` + isvc + `", "verb": "get"}}`, + `--openshift-sar={"namespace": "` + namespace + `", "resource": "inferenceservices", "group": "serving.kserve.io", "name": "` + isvc + `", "verb": "get"}`, + }, + Image: oauthImage, + 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(oauthCpuLimit), + corev1.ResourceMemory: resource.MustParse(oauthMemoryLimit), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse(oauthCpuRequest), + corev1.ResourceMemory: resource.MustParse(oauthMemoryRequest), + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: tlsVolumeName, + MountPath: "/etc/tls/private", + }, + }, + }, nil +} + +func generateCookieSecret() (string, error) { + secret := make([]byte, 32) + _, err := rand.Read(secret) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(secret), nil +} + // checkDeploymentExist checks if the deployment exists? func (r *DeploymentReconciler) checkDeploymentExist(client kclient.Client, deployment *appsv1.Deployment) (constants.CheckResultType, *appsv1.Deployment, error) { // get deployment @@ -203,7 +412,9 @@ func (r *DeploymentReconciler) checkDeploymentExist(client kclient.Client, deplo log.Error(err, "Failed to perform dry-run update of deployment", "Deployment", deployment.Name) return constants.CheckResultUnknown, nil, err } - if diff, err := kmp.SafeDiff(deployment.Spec, existingDeployment.Spec, ignoreFields); err != nil { + processedExistingDep := v1beta1utils.RemoveCookieSecretArg(*existingDeployment) + processedNewDep := v1beta1utils.RemoveCookieSecretArg(*deployment) + if diff, err := kmp.SafeDiff(processedNewDep.Spec, processedExistingDep.Spec, ignoreFields); err != nil { return constants.CheckResultUnknown, nil, err } else if diff != "" { log.Info("Deployment Updated", "Diff", diff) @@ -375,7 +586,7 @@ func (r *DeploymentReconciler) Reconcile() ([]*appsv1.Deployment, error) { } // Patch the deployment object with the strategic merge patch - opErr = r.client.Patch(context.TODO(), deployment, client.RawPatch(types.StrategicMergePatchType, patchByte)) + opErr = r.client.Patch(context.TODO(), deployment, kclient.RawPatch(types.StrategicMergePatchType, patchByte)) } if opErr != nil { diff --git a/pkg/controller/v1beta1/inferenceservice/reconcilers/deployment/deployment_reconciler_test.go b/pkg/controller/v1beta1/inferenceservice/reconcilers/deployment/deployment_reconciler_test.go index 0336bafbab6..c2f84f765e4 100644 --- a/pkg/controller/v1beta1/inferenceservice/reconcilers/deployment/deployment_reconciler_test.go +++ b/pkg/controller/v1beta1/inferenceservice/reconcilers/deployment/deployment_reconciler_test.go @@ -30,11 +30,13 @@ import ( "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/client-go/kubernetes" ) func TestCreateDefaultDeployment(t *testing.T) { type args struct { + clientset kubernetes.Interface objectMeta metav1.ObjectMeta workerObjectMeta metav1.ObjectMeta componentExt *v1beta1.ComponentExtensionSpec @@ -389,7 +391,14 @@ func TestCreateDefaultDeployment(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := createRawDeployment(tt.args.objectMeta, tt.args.workerObjectMeta, tt.args.componentExt, tt.args.podSpec, tt.args.workerPodSpec) + got, err := createRawDeployment(tt.args.objectMeta, tt.args.workerObjectMeta, tt.args.componentExt, tt.args.podSpec, tt.args.workerPodSpec) + if err != nil { + t.Error(err) + } + if len(got) == 0 { + t.Errorf("Got empty deployment") + } + for i, deploy := range got { if diff := cmp.Diff(tt.expected[i], deploy, cmpopts.IgnoreFields(appsv1.Deployment{}, "Spec.Template.Spec.SecurityContext"), cmpopts.IgnoreFields(appsv1.Deployment{}, "Spec.Template.Spec.RestartPolicy"), @@ -457,7 +466,13 @@ func TestCreateDefaultDeployment(t *testing.T) { ttExpected := getDefaultExpectedDeployment() // update objectMeta using modify func - got := createRawDeployment(ttArgs.objectMeta, ttArgs.workerObjectMeta, ttArgs.componentExt, tt.modifyArgs(ttArgs).podSpec, tt.modifyArgs(ttArgs).workerPodSpec) + got, err := createRawDeployment(ttArgs.objectMeta, ttArgs.workerObjectMeta, ttArgs.componentExt, tt.modifyArgs(ttArgs).podSpec, tt.modifyArgs(ttArgs).workerPodSpec) + if err != nil { + t.Error(err) + } + if len(got) == 0 { + t.Errorf("Got empty deployment") + } // update expected value using modifyExpected func expected := tt.modifyExpected(ttExpected) @@ -760,7 +775,13 @@ func TestCreateDefaultDeployment(t *testing.T) { ttExpected := getDefaultExpectedDeployment() // update objectMeta using modify func - got := createRawDeployment(tt.modifyObjectMetaArgs(ttArgs).objectMeta, tt.modifyWorkerObjectMetaArgs(ttArgs).workerObjectMeta, ttArgs.componentExt, tt.modifyPodSpecArgs(ttArgs).podSpec, tt.modifyWorkerPodSpecArgs(ttArgs).workerPodSpec) + got, err := createRawDeployment(tt.modifyObjectMetaArgs(ttArgs).objectMeta, tt.modifyWorkerObjectMetaArgs(ttArgs).workerObjectMeta, ttArgs.componentExt, tt.modifyPodSpecArgs(ttArgs).podSpec, tt.modifyWorkerPodSpecArgs(ttArgs).workerPodSpec) + if err != nil { + t.Error(err) + } + if len(got) == 0 { + t.Errorf("Got empty deployment") + } // update expected value using modifyExpected func expected := tt.modifyExpected(ttExpected) 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 f0d508415bc..28679560f00 100644 --- a/pkg/controller/v1beta1/inferenceservice/reconcilers/ingress/kube_ingress_reconciler.go +++ b/pkg/controller/v1beta1/inferenceservice/reconcilers/ingress/kube_ingress_reconciler.go @@ -19,9 +19,11 @@ package ingress import ( "context" "fmt" + "strconv" v1beta1 "github.com/kserve/kserve/pkg/apis/serving/v1beta1" "github.com/kserve/kserve/pkg/constants" + v1beta1utils "github.com/kserve/kserve/pkg/controller/v1beta1/inferenceservice/utils" "github.com/kserve/kserve/pkg/utils" corev1 "k8s.io/api/core/v1" netv1 "k8s.io/api/networking/v1" @@ -55,16 +57,39 @@ func NewRawIngressReconciler(client client.Client, }, nil } -func createRawURL(isvc *v1beta1.InferenceService, - ingressConfig *v1beta1.IngressConfig) (*knapis.URL, error) { - var err error +func createRawURL(client client.Client, isvc *v1beta1.InferenceService, authEnabled bool) (*knapis.URL, error) { + // upstream implementation + // var err error + // url := &knapis.URL{} + // url.Scheme = ingressConfig.UrlScheme + // url.Host, err = GenerateDomainName(isvc.Name, isvc.ObjectMeta, ingressConfig) + // if err != nil { + // return nil, err + // } + // if authEnabled { + // url.Host += ":" + strconv.Itoa(constants.OauthProxyPort) + // } + // return url, nil + + // ODH changes url := &knapis.URL{} - url.Scheme = ingressConfig.UrlScheme - url.Host, err = GenerateDomainName(isvc.Name, isvc.ObjectMeta, ingressConfig) - if err != nil { - return nil, err + if val, ok := isvc.Labels[constants.NetworkVisibility]; ok && val == constants.ODHRouteEnabled { + var err error + url, err = v1beta1utils.GetRouteURLIfExists(client, isvc.ObjectMeta, isvc.Name) + if err != nil { + return nil, err + } + } else { + url = &apis.URL{ + Host: getRawServiceHost(isvc, client), + Scheme: "http", + Path: "", + } + if authEnabled { + url.Host += ":" + strconv.Itoa(constants.OauthProxyPort) + url.Scheme = "https" + } } - return url, nil } @@ -304,16 +329,27 @@ func (r *RawIngressReconciler) Reconcile(isvc *v1beta1.InferenceService) error { return err } } - isvc.Status.URL, err = createRawURL(isvc, r.ingressConfig) + authEnabled := false + if val, ok := isvc.Labels[constants.ODHKserveRawAuth]; ok && val == "true" { + authEnabled = true + } + isvc.Status.URL, err = createRawURL(r.client, isvc, authEnabled) if err != nil { return err } + internalHost := getRawServiceHost(isvc, r.client) + url := &apis.URL{ + Host: internalHost, + Scheme: "http", + Path: "", + } + if authEnabled { + internalHost += ":" + strconv.Itoa(constants.OauthProxyPort) + url.Host = internalHost + url.Scheme = "https" + } isvc.Status.Address = &duckv1.Addressable{ - URL: &apis.URL{ - Host: getRawServiceHost(isvc, r.client), - Scheme: r.ingressConfig.UrlScheme, - Path: "", - }, + URL: url, } isvc.Status.SetCondition(v1beta1.IngressReady, &apis.Condition{ Type: v1beta1.IngressReady, 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 1f6e1821843..722c584c370 100644 --- a/pkg/controller/v1beta1/inferenceservice/reconcilers/raw/raw_kube_reconciler.go +++ b/pkg/controller/v1beta1/inferenceservice/reconcilers/raw/raw_kube_reconciler.go @@ -31,8 +31,12 @@ import ( "k8s.io/client-go/kubernetes" knapis "knative.dev/pkg/apis" "sigs.k8s.io/controller-runtime/pkg/client" + + logf "sigs.k8s.io/controller-runtime/pkg/log" ) +var log = logf.Log.WithName("RawKubeReconciler") + // RawKubeReconciler reconciles the Native K8S Resources type RawKubeReconciler struct { client client.Client @@ -64,11 +68,22 @@ func NewRawKubeReconciler(client client.Client, if workerPodSpec != nil { multiNodeEnabled = true } + + // do not return error as service config is optional + serviceConfig, err1 := v1beta1.NewServiceConfig(clientset) + if err1 != nil { + log.Error(err1, "failed to get service config") + } + + depl, err := deployment.NewDeploymentReconciler(client, clientset, scheme, componentMeta, workerComponentMeta, componentExt, podSpec, workerPodSpec) + if err != nil { + return nil, err + } return &RawKubeReconciler{ client: client, scheme: scheme, - Deployment: deployment.NewDeploymentReconciler(client, scheme, componentMeta, workerComponentMeta, componentExt, podSpec, workerPodSpec), - Service: service.NewServiceReconciler(client, scheme, componentMeta, componentExt, podSpec, multiNodeEnabled), + Deployment: depl, + Service: service.NewServiceReconciler(client, scheme, componentMeta, componentExt, podSpec, multiNodeEnabled, serviceConfig), Scaler: as, URL: url, }, nil @@ -86,19 +101,19 @@ func createRawURL(clientset kubernetes.Interface, metadata metav1.ObjectMeta) (* if err != nil { return nil, fmt.Errorf("failed creating host name: %w", err) } - return url, nil } // Reconcile ... func (r *RawKubeReconciler) Reconcile() ([]*appsv1.Deployment, error) { - // reconcile Deployment - deploymentList, 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 + deploymentList, 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 c171d07facb..58afe136b57 100644 --- a/pkg/controller/v1beta1/inferenceservice/reconcilers/service/service_reconciler.go +++ b/pkg/controller/v1beta1/inferenceservice/reconcilers/service/service_reconciler.go @@ -49,17 +49,18 @@ func NewServiceReconciler(client client.Client, scheme *runtime.Scheme, componentMeta metav1.ObjectMeta, componentExt *v1beta1.ComponentExtensionSpec, - podSpec *corev1.PodSpec, multiNodeEnabled bool) *ServiceReconciler { + podSpec *corev1.PodSpec, multiNodeEnabled bool, + serviceConfig *v1beta1.ServiceConfig) *ServiceReconciler { return &ServiceReconciler{ client: client, scheme: scheme, - ServiceList: createService(componentMeta, componentExt, podSpec, multiNodeEnabled), + ServiceList: createService(componentMeta, componentExt, podSpec, multiNodeEnabled, serviceConfig), componentExt: componentExt, } } func createService(componentMeta metav1.ObjectMeta, componentExt *v1beta1.ComponentExtensionSpec, - podSpec *corev1.PodSpec, multiNodeEnabled bool) []*corev1.Service { + podSpec *corev1.PodSpec, multiNodeEnabled bool, serviceConfig *v1beta1.ServiceConfig) []*corev1.Service { var svcList []*corev1.Service var isWorkerContainer bool @@ -73,11 +74,11 @@ func createService(componentMeta metav1.ObjectMeta, componentExt *v1beta1.Compon if !multiNodeEnabled { // If multiNodeEnabled is false, only defaultSvc will be created. - defaultSvc := createDefaultSvc(componentMeta, componentExt, podSpec) + defaultSvc := createDefaultSvc(componentMeta, componentExt, podSpec, serviceConfig) svcList = append(svcList, defaultSvc) } else if multiNodeEnabled && !isWorkerContainer { // If multiNodeEnabled is true, both defaultSvc and headSvc will be created. - defaultSvc := createDefaultSvc(componentMeta, componentExt, podSpec) + defaultSvc := createDefaultSvc(componentMeta, componentExt, podSpec, serviceConfig) svcList = append(svcList, defaultSvc) headSvc := createHeadlessSvc(componentMeta) @@ -88,7 +89,7 @@ func createService(componentMeta metav1.ObjectMeta, componentExt *v1beta1.Compon } func createDefaultSvc(componentMeta metav1.ObjectMeta, componentExt *v1beta1.ComponentExtensionSpec, - podSpec *corev1.PodSpec) *corev1.Service { + podSpec *corev1.PodSpec, serviceConfig *v1beta1.ServiceConfig) *corev1.Service { var servicePorts []corev1.ServicePort if len(podSpec.Containers) != 0 { @@ -110,6 +111,9 @@ func createDefaultSvc(componentMeta metav1.ObjectMeta, componentExt *v1beta1.Com }, Protocol: container.Ports[0].Protocol, } + if len(servicePort.Name) == 0 { + servicePort.Name = "http" + } servicePorts = append(servicePorts, servicePort) for i := 1; i < len(container.Ports); i++ { @@ -162,12 +166,40 @@ func createDefaultSvc(componentMeta metav1.ObjectMeta, componentExt *v1beta1.Com "app": constants.GetRawServiceLabel(componentMeta.Name), }, Ports: servicePorts, - // TODO - add a control flag - // Need to add a control flag to properly set it, enable/disable this behavior. - // Follow up issue to align with upstream: https://issues.redhat.com/browse/RHOAIENG-5077 - 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 + constants.ServingCertSecretSuffix + httpsPort := corev1.ServicePort{ + Name: "https", + Port: constants.OauthProxyPort, + TargetPort: intstr.IntOrString{ + Type: intstr.String, + StrVal: "https", + }, + Protocol: corev1.ProtocolTCP, + } + ports := service.Spec.Ports + replaced := false + for i, port := range ports { + if port.Port == constants.CommonDefaultHttpPort { + ports[i] = httpsPort + replaced = true + } + } + if !replaced { + ports = append(ports, httpsPort) + } + service.Spec.Ports = ports + } + + if serviceConfig != nil && serviceConfig.ServiceClusterIPNone { + service.Spec.ClusterIP = corev1.ClusterIPNone + } + return service } diff --git a/pkg/controller/v1beta1/inferenceservice/reconcilers/service/service_reconciler_test.go b/pkg/controller/v1beta1/inferenceservice/reconcilers/service/service_reconciler_test.go index 27846174f8d..37e285c9953 100644 --- a/pkg/controller/v1beta1/inferenceservice/reconcilers/service/service_reconciler_test.go +++ b/pkg/controller/v1beta1/inferenceservice/reconcilers/service/service_reconciler_test.go @@ -21,11 +21,14 @@ import ( "github.com/google/go-cmp/cmp" "github.com/kserve/kserve/pkg/apis/serving/v1beta1" "github.com/kserve/kserve/pkg/constants" + "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" ) +var emptyServiceConfig = &v1beta1.ServiceConfig{} + func TestCreateDefaultDeployment(t *testing.T) { type args struct { @@ -127,7 +130,6 @@ func TestCreateDefaultDeployment(t *testing.T) { TargetPort: intstr.IntOrString{IntVal: 8080}, }, }, - ClusterIP: corev1.ClusterIPNone, Selector: map[string]string{ constants.RawDeploymentAppLabel: "isvc.default-predictor", }, @@ -159,7 +161,6 @@ func TestCreateDefaultDeployment(t *testing.T) { TargetPort: intstr.IntOrString{IntVal: 8080}, }, }, - ClusterIP: corev1.ClusterIPNone, Selector: map[string]string{ "app": "isvc.default-predictor", }, @@ -185,7 +186,7 @@ func TestCreateDefaultDeployment(t *testing.T) { constants.RawDeploymentAppLabel: "isvc.default-predictor", constants.InferenceServiceGenerationPodLabelKey: "1", }, - ClusterIP: "None", + ClusterIP: corev1.ClusterIPNone, PublishNotReadyAddresses: true, }, }, @@ -220,7 +221,7 @@ func TestCreateDefaultDeployment(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := createService(tt.args.componentMeta, tt.args.componentExt, tt.args.podSpec, tt.args.multiNodeEnabled) + got := createService(tt.args.componentMeta, tt.args.componentExt, tt.args.podSpec, tt.args.multiNodeEnabled, emptyServiceConfig) for i, service := range got { if diff := cmp.Diff(tt.expected[i], service); diff != "" { t.Errorf("Test %q unexpected service (-want +got): %v", tt.name, diff) @@ -230,3 +231,52 @@ func TestCreateDefaultDeployment(t *testing.T) { }) } } + +func TestCreateServiceRawServiceConfigEmpty(t *testing.T) { + // nothing expected + runTestServiceCreate(emptyServiceConfig, "", t) +} + +func TestCreateServiceRawServiceAndConfigNil(t *testing.T) { + serviceConfig := &v1beta1.ServiceConfig{} + serviceConfig = nil + // no service means empty + runTestServiceCreate(serviceConfig, "", t) +} + +func TestCreateServiceRawFalseAndConfigTrue(t *testing.T) { + serviceConfig := &v1beta1.ServiceConfig{ + ServiceClusterIPNone: true, + } + runTestServiceCreate(serviceConfig, corev1.ClusterIPNone, t) +} + +func TestCreateServiceRawTrueAndConfigFalse(t *testing.T) { + serviceConfig := &v1beta1.ServiceConfig{ + ServiceClusterIPNone: false, + } + runTestServiceCreate(serviceConfig, "", t) +} + +func TestCreateServiceRawFalseAndConfigNil(t *testing.T) { + runTestServiceCreate(emptyServiceConfig, "", t) +} + +func TestCreateServiceRawTrueAndConfigNil(t *testing.T) { + // service is there, but no property, should be empty + runTestServiceCreate(emptyServiceConfig, "", t) +} + +func runTestServiceCreate(serviceConfig *v1beta1.ServiceConfig, expectedClusterIP string, t *testing.T) { + componentMeta := metav1.ObjectMeta{ + Name: "test-service", + Namespace: "default", + } + componentExt := &v1beta1.ComponentExtensionSpec{} + podSpec := &corev1.PodSpec{} + + service := createService(componentMeta, componentExt, podSpec, false, serviceConfig) + assert.Equal(t, componentMeta, service[0].ObjectMeta, "Expected ObjectMeta to be equal") + assert.Equal(t, map[string]string{"app": "isvc.test-service"}, service[0].Spec.Selector, "Expected Selector to be equal") + assert.Equal(t, expectedClusterIP, service[0].Spec.ClusterIP, "Expected ClusterIP to be equal") +} diff --git a/pkg/controller/v1beta1/inferenceservice/suite_test.go b/pkg/controller/v1beta1/inferenceservice/suite_test.go index 19fe37294e3..058ce3f7d54 100644 --- a/pkg/controller/v1beta1/inferenceservice/suite_test.go +++ b/pkg/controller/v1beta1/inferenceservice/suite_test.go @@ -41,6 +41,7 @@ import ( "github.com/kserve/kserve/pkg/apis/serving/v1beta1" "github.com/kserve/kserve/pkg/constants" pkgtest "github.com/kserve/kserve/pkg/testing" + routev1 "github.com/openshift/api/route/v1" ) // These tests use Ginkgo (BDD-style Go testing framework). Refer to @@ -81,6 +82,8 @@ var _ = BeforeSuite(func() { Expect(err).NotTo(HaveOccurred()) err = netv1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) + err = routev1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) Expect(err).ToNot(HaveOccurred()) diff --git a/pkg/controller/v1beta1/inferenceservice/utils/utils.go b/pkg/controller/v1beta1/inferenceservice/utils/utils.go index 7c697009159..38f68c38b41 100644 --- a/pkg/controller/v1beta1/inferenceservice/utils/utils.go +++ b/pkg/controller/v1beta1/inferenceservice/utils/utils.go @@ -26,6 +26,11 @@ import ( "sort" "strings" + routev1 "github.com/openshift/api/route/v1" + appsv1 "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/types" + "knative.dev/pkg/apis" + "github.com/pkg/errors" goerrors "github.com/pkg/errors" v1 "k8s.io/api/core/v1" @@ -441,3 +446,67 @@ func MergeServingRuntimeAndInferenceServiceSpecs(srContainers []v1.Container, is } return containerIndexInSR, mergedContainer, mergedPodSpec, nil } + +// Since the "cookie-secret" arg in the oauth proxy container is generated randomly, +// we exclude that arg while comparing existing and desired deployment specs +func RemoveCookieSecretArg(deployment appsv1.Deployment) *appsv1.Deployment { + dep := deployment.DeepCopy() + for i, container := range dep.Spec.Template.Spec.Containers { + if container.Name == "oauth-proxy" { + var newArgs []string + for _, arg := range container.Args { + if !strings.Contains(arg, "cookie-secret") { + newArgs = append(newArgs, arg) + } + } + dep.Spec.Template.Spec.Containers[i].Args = newArgs + } + } + return dep +} + +// Check for route created by odh-model-controller. If the route is found, use it as the isvc URL +func GetRouteURLIfExists(cli client.Client, metadata metav1.ObjectMeta, isvcName string) (*apis.URL, error) { + foundRoute := false + routeReady := false + route := &routev1.Route{} + err := cli.Get(context.TODO(), types.NamespacedName{Name: isvcName, Namespace: metadata.Namespace}, route) + if err != nil { + return nil, err + } + + // Check if the route is owned by the InferenceService + for _, ownerRef := range route.OwnerReferences { + if ownerRef.UID == metadata.UID { + foundRoute = true + } + } + + // Check if the route is admitted + for _, ingress := range route.Status.Ingress { + for _, condition := range ingress.Conditions { + if condition.Type == "Admitted" && condition.Status == "True" { + routeReady = true + } + } + } + + if !(foundRoute && routeReady) { + return nil, fmt.Errorf("route %s/%s not found or not ready", metadata.Namespace, metadata.Name) + } + + // Construct the URL + host := route.Spec.Host + scheme := "http" + if route.Spec.TLS != nil && route.Spec.TLS.Termination != "" { + scheme = "https" + } + + // Create the URL as an apis.URL object + routeURL := &apis.URL{ + Scheme: scheme, + Host: host, + } + + return routeURL, nil +} diff --git a/pkg/openapi/openapi_generated.go b/pkg/openapi/openapi_generated.go index c8b846852d6..af15eb8280a 100644 --- a/pkg/openapi/openapi_generated.go +++ b/pkg/openapi/openapi_generated.go @@ -95,6 +95,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/kserve/kserve/pkg/apis/serving/v1beta1.PredictorSpec": schema_pkg_apis_serving_v1beta1_PredictorSpec(ref), "github.com/kserve/kserve/pkg/apis/serving/v1beta1.SKLearnSpec": schema_pkg_apis_serving_v1beta1_SKLearnSpec(ref), "github.com/kserve/kserve/pkg/apis/serving/v1beta1.SecurityConfig": schema_pkg_apis_serving_v1beta1_SecurityConfig(ref), + "github.com/kserve/kserve/pkg/apis/serving/v1beta1.ServiceConfig": schema_pkg_apis_serving_v1beta1_ServiceConfig(ref), "github.com/kserve/kserve/pkg/apis/serving/v1beta1.StorageSpec": schema_pkg_apis_serving_v1beta1_StorageSpec(ref), "github.com/kserve/kserve/pkg/apis/serving/v1beta1.TFServingSpec": schema_pkg_apis_serving_v1beta1_TFServingSpec(ref), "github.com/kserve/kserve/pkg/apis/serving/v1beta1.TorchServeSpec": schema_pkg_apis_serving_v1beta1_TorchServeSpec(ref), @@ -9071,6 +9072,25 @@ func schema_pkg_apis_serving_v1beta1_SecurityConfig(ref common.ReferenceCallback } } +func schema_pkg_apis_serving_v1beta1_ServiceConfig(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "serviceClusterIPNone": { + SchemaProps: spec.SchemaProps{ + Description: "ServiceClusterIPNone is a boolean flag to indicate if the service should have a clusterIP set to None. If the DeploymentMode is Raw, the default value for ServiceClusterIPNone is false when the value is absent.", + Type: []string{"boolean"}, + Format: "", + }, + }, + }, + }, + }, + } +} + func schema_pkg_apis_serving_v1beta1_StorageSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/pkg/openapi/swagger.json b/pkg/openapi/swagger.json index ea2e2395e3f..bac93c9fa7c 100644 --- a/pkg/openapi/swagger.json +++ b/pkg/openapi/swagger.json @@ -5055,6 +5055,15 @@ } } }, + "v1beta1.ServiceConfig": { + "type": "object", + "properties": { + "serviceClusterIPNone": { + "description": "ServiceClusterIPNone is a boolean flag to indicate if the service should have a clusterIP set to None. If the DeploymentMode is Raw, the default value for ServiceClusterIPNone is false when the value is absent.", + "type": "boolean" + } + } + }, "v1beta1.StorageSpec": { "type": "object", "properties": { diff --git a/python/kserve/docs/V1beta1ServiceConfig.md b/python/kserve/docs/V1beta1ServiceConfig.md new file mode 100644 index 00000000000..51e99d468be --- /dev/null +++ b/python/kserve/docs/V1beta1ServiceConfig.md @@ -0,0 +1,10 @@ +# V1beta1ServiceConfig + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**service_cluster_ip_none** | **bool** | ServiceClusterIPNone is a boolean flag to indicate if the service should have a clusterIP set to None. If the DeploymentMode is Raw, the default value for ServiceClusterIPNone is false when the value is absent. | [optional] + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/python/kserve/kserve/models/__init__.py b/python/kserve/kserve/models/__init__.py index 71cb32c8eac..0261f1faeca 100644 --- a/python/kserve/kserve/models/__init__.py +++ b/python/kserve/kserve/models/__init__.py @@ -94,6 +94,7 @@ from kserve.models.v1beta1_predictor_spec import V1beta1PredictorSpec from kserve.models.v1beta1_sk_learn_spec import V1beta1SKLearnSpec from kserve.models.v1beta1_security_config import V1beta1SecurityConfig +from kserve.models.v1beta1_service_config import V1beta1ServiceConfig from kserve.models.v1beta1_storage_spec import V1beta1StorageSpec from kserve.models.v1beta1_tf_serving_spec import V1beta1TFServingSpec from kserve.models.v1beta1_torch_serve_spec import V1beta1TorchServeSpec diff --git a/python/kserve/kserve/models/v1beta1_service_config.py b/python/kserve/kserve/models/v1beta1_service_config.py new file mode 100644 index 00000000000..f30e70f6d2c --- /dev/null +++ b/python/kserve/kserve/models/v1beta1_service_config.py @@ -0,0 +1,136 @@ +# Copyright 2023 The KServe Authors. +# +# 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. + +# coding: utf-8 + +""" + KServe + + Python SDK for KServe # noqa: E501 + + The version of the OpenAPI document: v0.1 + Generated by: https://openapi-generator.tech +""" + + +import pprint +import re # noqa: F401 + +import six + +from kserve.configuration import Configuration + + +class V1beta1ServiceConfig(object): + """NOTE: This class is auto generated by OpenAPI Generator. + Ref: https://openapi-generator.tech + + Do not edit the class manually. + """ + + """ + Attributes: + openapi_types (dict): The key is attribute name + and the value is attribute type. + attribute_map (dict): The key is attribute name + and the value is json key in definition. + """ + openapi_types = { + 'service_cluster_ip_none': 'bool' + } + + attribute_map = { + 'service_cluster_ip_none': 'serviceClusterIPNone' + } + + def __init__(self, service_cluster_ip_none=None, local_vars_configuration=None): # noqa: E501 + """V1beta1ServiceConfig - a model defined in OpenAPI""" # noqa: E501 + if local_vars_configuration is None: + local_vars_configuration = Configuration() + self.local_vars_configuration = local_vars_configuration + + self._service_cluster_ip_none = None + self.discriminator = None + + if service_cluster_ip_none is not None: + self.service_cluster_ip_none = service_cluster_ip_none + + @property + def service_cluster_ip_none(self): + """Gets the service_cluster_ip_none of this V1beta1ServiceConfig. # noqa: E501 + + ServiceClusterIPNone is a boolean flag to indicate if the service should have a clusterIP set to None. If the DeploymentMode is Raw, the default value for ServiceClusterIPNone is false when the value is absent. # noqa: E501 + + :return: The service_cluster_ip_none of this V1beta1ServiceConfig. # noqa: E501 + :rtype: bool + """ + return self._service_cluster_ip_none + + @service_cluster_ip_none.setter + def service_cluster_ip_none(self, service_cluster_ip_none): + """Sets the service_cluster_ip_none of this V1beta1ServiceConfig. + + ServiceClusterIPNone is a boolean flag to indicate if the service should have a clusterIP set to None. If the DeploymentMode is Raw, the default value for ServiceClusterIPNone is false when the value is absent. # noqa: E501 + + :param service_cluster_ip_none: The service_cluster_ip_none of this V1beta1ServiceConfig. # noqa: E501 + :type: bool + """ + + self._service_cluster_ip_none = service_cluster_ip_none + + def to_dict(self): + """Returns the model properties as a dict""" + result = {} + + for attr, _ in six.iteritems(self.openapi_types): + value = getattr(self, attr) + if isinstance(value, list): + result[attr] = list(map( + lambda x: x.to_dict() if hasattr(x, "to_dict") else x, + value + )) + elif hasattr(value, "to_dict"): + result[attr] = value.to_dict() + elif isinstance(value, dict): + result[attr] = dict(map( + lambda item: (item[0], item[1].to_dict()) + if hasattr(item[1], "to_dict") else item, + value.items() + )) + else: + result[attr] = value + + return result + + def to_str(self): + """Returns the string representation of the model""" + return pprint.pformat(self.to_dict()) + + def __repr__(self): + """For `print` and `pprint`""" + return self.to_str() + + def __eq__(self, other): + """Returns true if both objects are equal""" + if not isinstance(other, V1beta1ServiceConfig): + return False + + return self.to_dict() == other.to_dict() + + def __ne__(self, other): + """Returns true if both objects are not equal""" + if not isinstance(other, V1beta1ServiceConfig): + return True + + return self.to_dict() != other.to_dict() diff --git a/python/kserve/test/test_v1beta1_service_config.py b/python/kserve/test/test_v1beta1_service_config.py new file mode 100644 index 00000000000..1b239307f7f --- /dev/null +++ b/python/kserve/test/test_v1beta1_service_config.py @@ -0,0 +1,64 @@ +# Copyright 2023 The KServe Authors. +# +# 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. + +# coding: utf-8 + +""" + KServe + + Python SDK for KServe # noqa: E501 + + The version of the OpenAPI document: v0.1 + Generated by: https://openapi-generator.tech +""" + + +from __future__ import absolute_import + +import unittest +import datetime + +import kserve +from kserve.models.v1beta1_service_config import V1beta1ServiceConfig # noqa: E501 +from kserve.rest import ApiException + + +class TestV1beta1ServiceConfig(unittest.TestCase): + """V1beta1ServiceConfig unit test stubs""" + + def setUp(self): + pass + + def tearDown(self): + pass + + def make_instance(self, include_optional): + """Test V1beta1ServiceConfig + include_option is a boolean, when False only required + params are included, when True both required and + optional params are included""" + # model = kserve.models.v1beta1_service_config.V1beta1ServiceConfig() # noqa: E501 + if include_optional: + return V1beta1ServiceConfig(service_cluster_ip_none=True) + else: + return V1beta1ServiceConfig() + + def testV1beta1ServiceConfig(self): + """Test V1beta1ServiceConfig""" + inst_req_only = self.make_instance(include_optional=False) + inst_req_and_optional = self.make_instance(include_optional=True) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/crds/route.openshift.io_routes.yaml b/test/crds/route.openshift.io_routes.yaml new file mode 100644 index 00000000000..314ff2fc410 --- /dev/null +++ b/test/crds/route.openshift.io_routes.yaml @@ -0,0 +1,256 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.8.0 + creationTimestamp: null + name: routes.route.openshift.io +spec: + group: route.openshift.io + names: + kind: Route + listKind: RouteList + plural: routes + singular: route + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: "A route allows developers to expose services through an HTTP(S) + aware load balancing and proxy layer via a public DNS entry. The route may + further specify TLS options and a certificate, or specify a public CNAME + that the router should also accept for HTTP and HTTPS traffic. An administrator + typically configures their router to be visible outside the cluster firewall, + and may also add additional security, caching, or traffic controls on the + service content. Routers usually talk directly to the service endpoints. + \n Once a route is created, the `host` field may not be changed. Generally, + routers use the oldest route with a given host when resolving conflicts. + \n Routers are subject to additional customization and may support additional + controls via the annotations field. \n Because administrators may configure + multiple routers, the route status field is used to return information to + clients about the names and states of the route under each router. If a + client chooses a duplicate name, for instance, the route status conditions + are used to indicate the route cannot be chosen." + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: spec is the desired state of the route + properties: + alternateBackends: + description: alternateBackends allows up to 3 additional backends + to be assigned to the route. Only the Service kind is allowed, and + it will be defaulted to Service. Use the weight field in RouteTargetReference + object to specify relative preference. + items: + description: RouteTargetReference specifies the target that resolve + into endpoints. Only the 'Service' kind is allowed. Use 'weight' + field to emphasize one over others. + properties: + kind: + description: The kind of target that the route is referring + to. Currently, only 'Service' is allowed + type: string + name: + description: name of the service/target that is being referred + to. e.g. name of the service + type: string + weight: + description: weight as an integer between 0 and 256, default + 1, that specifies the target's relative weight against other + target reference objects. 0 suppresses requests to this backend. + format: int32 + type: integer + required: + - kind + - name + - weight + type: object + type: array + host: + description: host is an alias/DNS that points to the service. Optional. + If not specified a route name will typically be automatically chosen. + Must follow DNS952 subdomain conventions. + type: string + path: + description: Path that the router watches for, to route traffic for + to the service. Optional + type: string + port: + description: If specified, the port to be used by the router. Most + routers will use all endpoints exposed by the service by default + - set this value to instruct routers which port to use. + properties: + targetPort: + anyOf: + - type: integer + - type: string + description: The target port on pods selected by the service this + route points to. If this is a string, it will be looked up as + a named port in the target endpoints port list. Required + x-kubernetes-int-or-string: true + required: + - targetPort + type: object + tls: + description: The tls field provides the ability to configure certificates + and termination for the route. + properties: + caCertificate: + description: caCertificate provides the cert authority certificate + contents + type: string + certificate: + description: certificate provides certificate contents + type: string + destinationCACertificate: + description: destinationCACertificate provides the contents of + the ca certificate of the final destination. When using reencrypt + termination this file should be provided in order to have routers + use it for health checks on the secure connection. If this field + is not specified, the router may provide its own destination + CA and perform hostname validation using the short service name + (service.namespace.svc), which allows infrastructure generated + certificates to automatically verify. + type: string + insecureEdgeTerminationPolicy: + description: "insecureEdgeTerminationPolicy indicates the desired + behavior for insecure connections to a route. While each router + may make its own decisions on which ports to expose, this is + normally port 80. \n * Allow - traffic is sent to the server + on the insecure port (default) * Disable - no traffic is allowed + on the insecure port. * Redirect - clients are redirected to + the secure port." + type: string + key: + description: key provides key file contents + type: string + termination: + description: termination indicates termination type. + type: string + required: + - termination + type: object + to: + description: to is an object the route should use as the primary backend. + Only the Service kind is allowed, and it will be defaulted to Service. + If the weight field (0-256 default 1) is set to zero, no traffic + will be sent to this backend. + properties: + kind: + description: The kind of target that the route is referring to. + Currently, only 'Service' is allowed + type: string + name: + description: name of the service/target that is being referred + to. e.g. name of the service + type: string + weight: + description: weight as an integer between 0 and 256, default 1, + that specifies the target's relative weight against other target + reference objects. 0 suppresses requests to this backend. + format: int32 + type: integer + required: + - kind + - name + - weight + type: object + wildcardPolicy: + description: Wildcard policy if any for the route. Currently only + 'Subdomain' or 'None' is allowed. + type: string + required: + - host + - to + type: object + status: + description: status is the current state of the route + properties: + ingress: + description: ingress describes the places where the route may be exposed. + The list of ingress points may contain duplicate Host or RouterName + values. Routes are considered live once they are `Ready` + items: + description: RouteIngress holds information about the places where + a route is exposed. + properties: + conditions: + description: Conditions is the state of the route, may be empty. + items: + description: RouteIngressCondition contains details for the + current condition of this route on a particular router. + properties: + lastTransitionTime: + description: RFC 3339 date and time when this condition + last transitioned + format: date-time + type: string + message: + description: Human readable message indicating details + about last transition. + type: string + reason: + description: (brief) reason for the condition's last transition, + and is usually a machine and human readable constant + type: string + status: + description: Status is the status of the condition. Can + be True, False, Unknown. + type: string + type: + description: Type is the type of the condition. Currently + only Ready. + type: string + required: + - status + - type + type: object + type: array + host: + description: Host is the host string under which the route is + exposed; this value is required + type: string + routerCanonicalHostname: + description: CanonicalHostname is the external host name for + the router that can be used as a CNAME for the host requested + for this route. This value is optional and may not be set + in all cases. + type: string + routerName: + description: Name is a name chosen by the router to identify + itself; this value is required + type: string + wildcardPolicy: + description: Wildcard policy is the wildcard policy that was + allowed where this route is exposed. + type: string + type: object + type: array + required: + - ingress + type: object + required: + - spec + - status + type: object + served: true + storage: true +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/test/e2e/logger/test_raw_logger.py b/test/e2e/logger/test_raw_logger.py index fea1e69b8b0..e8c8fa06fc0 100644 --- a/test/e2e/logger/test_raw_logger.py +++ b/test/e2e/logger/test_raw_logger.py @@ -15,13 +15,15 @@ import os from kubernetes import client -from kserve import KServeClient -from kserve import constants -from kserve import V1beta1PredictorSpec -from kserve import V1beta1SKLearnSpec -from kserve import V1beta1InferenceServiceSpec -from kserve import V1beta1InferenceService -from kserve import V1beta1LoggerSpec +from kserve import ( + KServeClient, + constants, + V1beta1PredictorSpec, + V1beta1SKLearnSpec, + V1beta1InferenceServiceSpec, + V1beta1InferenceService, + V1beta1LoggerSpec, +) from kubernetes.client import V1ResourceRequirements from kubernetes.client import V1Container import pytest @@ -30,14 +32,67 @@ kserve_client = KServeClient(config_file=os.environ.get("KUBECONFIG", "~/.kube/config")) +annotations = {"serving.kserve.io/deploymentMode": "RawDeployment"} @pytest.mark.raw @pytest.mark.asyncio(scope="session") async def test_kserve_logger(rest_v1_client): msg_dumper = "message-dumper-raw" - annotations = {"serving.kserve.io/deploymentMode": "RawDeployment"} + before(msg_dumper) + service_name = "isvc-logger-raw" + predictor = V1beta1PredictorSpec( + min_replicas=1, + logger=V1beta1LoggerSpec( + mode="all", + url="http://" + + msg_dumper + + "-predictor" + + "." + + KSERVE_TEST_NAMESPACE + + ".svc.cluster.local", + ), + sklearn=V1beta1SKLearnSpec( + storage_uri="gs://kfserving-examples/models/sklearn/1.0/model", + resources=V1ResourceRequirements( + requests={"cpu": "10m", "memory": "128Mi"}, + limits={"cpu": "100m", "memory": "256Mi"}, + ), + ), + ) + base_test(msg_dumper, service_name, predictor, rest_v1_client) + + +@pytest.mark.rawcipn +async def test_kserve_logger_cipn(rest_v1_client): + msg_dumper = "message-dumper-raw-cipn" + before(msg_dumper) + + service_name = "isvc-logger-raw-cipn" + predictor = V1beta1PredictorSpec( + min_replicas=1, + logger=V1beta1LoggerSpec( + mode="all", + url="http://" + + msg_dumper + + "-predictor" + + "." + + KSERVE_TEST_NAMESPACE + + ".svc.cluster.local:8080", + ), + sklearn=V1beta1SKLearnSpec( + storage_uri="gs://kfserving-examples/models/sklearn/1.0/model", + resources=V1ResourceRequirements( + requests={"cpu": "10m", "memory": "128Mi"}, + limits={"cpu": "100m", "memory": "256Mi"}, + ), + ), + ) + await base_test(msg_dumper, service_name, predictor, rest_v1_client) + + +def before(msg_dumper): predictor = V1beta1PredictorSpec( min_replicas=1, containers=[ @@ -64,27 +119,8 @@ async def test_kserve_logger(rest_v1_client): kserve_client.create(isvc) kserve_client.wait_isvc_ready(msg_dumper, namespace=KSERVE_TEST_NAMESPACE) - service_name = "isvc-logger-raw" - predictor = V1beta1PredictorSpec( - min_replicas=1, - logger=V1beta1LoggerSpec( - mode="all", - url="http://" - + msg_dumper - + "-predictor" - + "." - + KSERVE_TEST_NAMESPACE - + ".svc.cluster.local", - ), - sklearn=V1beta1SKLearnSpec( - storage_uri="gs://kfserving-examples/models/sklearn/1.0/model", - resources=V1ResourceRequirements( - requests={"cpu": "10m", "memory": "128Mi"}, - limits={"cpu": "100m", "memory": "256Mi"}, - ), - ), - ) +async def base_test(msg_dumper, service_name, predictor, rest_v1_client): isvc = V1beta1InferenceService( api_version=constants.KSERVE_V1BETA1, kind=constants.KSERVE_KIND, @@ -120,10 +156,8 @@ async def test_kserve_logger(rest_v1_client): container="kserve-container", ) print(log) - # TODO, as part of the https://issues.redhat.com/browse/RHOAIENG-5077 - # add the control flag here to check the logs when headless service is disabled - # assert ("org.kubeflow.serving.inference.request" in log) - # assert ("org.kubeflow.serving.inference.response" in log) + assert "org.kubeflow.serving.inference.request" in log + assert "org.kubeflow.serving.inference.response" in log kserve_client.delete(service_name, KSERVE_TEST_NAMESPACE) kserve_client.delete(msg_dumper, KSERVE_TEST_NAMESPACE) diff --git a/test/e2e/pytest.ini b/test/e2e/pytest.ini index f1984989c16..8640452c7b4 100644 --- a/test/e2e/pytest.ini +++ b/test/e2e/pytest.ini @@ -10,6 +10,7 @@ markers = graph: inference graph tests helm: helm e2e tests raw: raw e2e tests + rawcipn: raw e2e tests with cluster ip none kourier: e2e tests using kourier as networking layer collocation: transformer and predictor collocation e2e tests predictor: predictor e2e tests including grpc