From 9ef5da357856168735b18bd2b2bb1de1aefadc0c Mon Sep 17 00:00:00 2001 From: Tommy Hughes Date: Thu, 13 Feb 2025 15:18:48 -0600 Subject: [PATCH] feast cronJob Signed-off-by: Tommy Hughes --- examples/operator-quickstart/01-Install.ipynb | 70 +- examples/operator-quickstart/02-Demo.ipynb | 457 ++++----- .../operator-quickstart/03-Uninstall.ipynb | 25 +- infra/feast-operator/Makefile | 3 +- .../api/v1alpha1/featurestore_types.go | 179 +++- .../api/v1alpha1/zz_generated.deepcopy.go | 147 +++ .../crd/bases/feast.dev_featurestores.yaml | 947 +++++++++++++++-- .../default/related_image_fs_patch.tmpl | 5 + .../default/related_image_fs_patch.yaml | 5 + .../config/manager/manager.yaml | 2 + .../config/overlays/odh/kustomization.yaml | 10 + .../config/overlays/odh/params.env | 1 + infra/feast-operator/config/rbac/role.yaml | 19 + infra/feast-operator/dist/install.yaml | 968 ++++++++++++++++-- infra/feast-operator/docs/api/markdown/ref.md | 173 ++++ .../internal/controller/authz/authz.go | 3 +- .../controller/featurestore_controller.go | 6 +- .../featurestore_controller_cronjob_test.go | 327 ++++++ .../featurestore_controller_test.go | 11 +- .../internal/controller/services/cronjob.go | 289 ++++++ .../internal/controller/services/services.go | 99 +- .../controller/services/services_types.go | 18 +- .../internal/controller/services/util.go | 13 + 23 files changed, 3205 insertions(+), 572 deletions(-) create mode 100644 infra/feast-operator/internal/controller/featurestore_controller_cronjob_test.go create mode 100644 infra/feast-operator/internal/controller/services/cronjob.go diff --git a/examples/operator-quickstart/01-Install.ipynb b/examples/operator-quickstart/01-Install.ipynb index c6655017144..624ba594296 100644 --- a/examples/operator-quickstart/01-Install.ipynb +++ b/examples/operator-quickstart/01-Install.ipynb @@ -74,7 +74,7 @@ "output_type": "stream", "text": [ "NAME STATUS AGE\n", - "feast Active 6s\n" + "feast Active 3s\n" ] } ], @@ -141,20 +141,20 @@ "output_type": "stream", "text": [ "NAME READY STATUS RESTARTS AGE\n", - "pod/postgres-ff8d4cf48-c4znd 1/1 Running 0 2m17s\n", - "pod/redis-b4756b75d-r9nfb 1/1 Running 0 2m15s\n", + "pod/postgres-ff8d4cf48-rvp86 1/1 Running 0 71s\n", + "pod/redis-b4756b75d-m5l96 1/1 Running 0 70s\n", "\n", "NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE\n", - "service/postgres ClusterIP 10.43.151.129 5432/TCP 2m17s\n", - "service/redis ClusterIP 10.43.169.233 6379/TCP 2m15s\n", + "service/postgres ClusterIP 10.43.193.169 5432/TCP 71s\n", + "service/redis ClusterIP 10.43.64.8 6379/TCP 69s\n", "\n", "NAME READY UP-TO-DATE AVAILABLE AGE\n", - "deployment.apps/postgres 1/1 1 1 2m18s\n", - "deployment.apps/redis 1/1 1 1 2m16s\n", + "deployment.apps/postgres 1/1 1 1 71s\n", + "deployment.apps/redis 1/1 1 1 70s\n", "\n", "NAME DESIRED CURRENT READY AGE\n", - "replicaset.apps/postgres-ff8d4cf48 1 1 1 2m18s\n", - "replicaset.apps/redis-b4756b75d 1 1 1 2m16s\n" + "replicaset.apps/postgres-ff8d4cf48 1 1 1 71s\n", + "replicaset.apps/redis-b4756b75d 1 1 1 70s\n" ] } ], @@ -217,7 +217,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 1, "metadata": {}, "outputs": [ { @@ -243,32 +243,35 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "NAME READY STATUS RESTARTS AGE\n", - "pod/feast-example-bbdc6cb6-rzkb4 0/1 Init:0/1 0 3s\n", - "pod/postgres-ff8d4cf48-c4znd 1/1 Running 0 4m49s\n", - "pod/redis-b4756b75d-r9nfb 1/1 Running 0 4m47s\n", + "NAME READY STATUS RESTARTS AGE\n", + "pod/feast-example-6c6b58474-n62rg 0/1 Init:0/1 0 4s\n", + "pod/postgres-ff8d4cf48-rvp86 1/1 Running 0 3m23s\n", + "pod/redis-b4756b75d-m5l96 1/1 Running 0 3m22s\n", "\n", "NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE\n", - "service/feast-example-online ClusterIP 10.43.143.216 80/TCP 4s\n", - "service/postgres ClusterIP 10.43.151.129 5432/TCP 4m49s\n", - "service/redis ClusterIP 10.43.169.233 6379/TCP 4m47s\n", + "service/feast-example-online ClusterIP 10.43.175.253 80/TCP 6s\n", + "service/postgres ClusterIP 10.43.193.169 5432/TCP 3m23s\n", + "service/redis ClusterIP 10.43.64.8 6379/TCP 3m21s\n", "\n", "NAME READY UP-TO-DATE AVAILABLE AGE\n", "deployment.apps/feast-example 0/1 1 0 5s\n", - "deployment.apps/postgres 1/1 1 1 4m51s\n", - "deployment.apps/redis 1/1 1 1 4m49s\n", + "deployment.apps/postgres 1/1 1 1 3m23s\n", + "deployment.apps/redis 1/1 1 1 3m22s\n", "\n", - "NAME DESIRED CURRENT READY AGE\n", - "replicaset.apps/feast-example-bbdc6cb6 1 1 0 4s\n", - "replicaset.apps/postgres-ff8d4cf48 1 1 1 4m51s\n", - "replicaset.apps/redis-b4756b75d 1 1 1 4m49s\n", + "NAME DESIRED CURRENT READY AGE\n", + "replicaset.apps/feast-example-6c6b58474 1 1 0 5s\n", + "replicaset.apps/postgres-ff8d4cf48 1 1 1 3m23s\n", + "replicaset.apps/redis-b4756b75d 1 1 1 3m22s\n", + "\n", + "NAME SCHEDULE TIMEZONE SUSPEND ACTIVE LAST SCHEDULE AGE\n", + "cronjob.batch/feast-example @yearly True 0 3s\n", "deployment.apps/feast-example condition met\n" ] } @@ -287,7 +290,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 4, "metadata": {}, "outputs": [ { @@ -295,7 +298,7 @@ "output_type": "stream", "text": [ "NAME STATUS AGE\n", - "example Ready 48m\n" + "example Ready 7m40s\n" ] } ], @@ -312,7 +315,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 1, "metadata": {}, "outputs": [ { @@ -352,29 +355,20 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "/opt/app-root/lib64/python3.11/site-packages/feast/feature_view.py:48: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity '__dummy'.\n", - " DUMMY_ENTITY = Entity(\n", - "Feast SDK Version: \"0.46.0\"\n" + "Feast SDK Version: \"0.1.dev1+gcc1fcad.d20250403\"\n" ] } ], "source": [ "!kubectl exec deployment/feast-example -itc online -- feast version" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/operator-quickstart/02-Demo.ipynb b/examples/operator-quickstart/02-Demo.ipynb index 536e36f490f..1ae6f23ddba 100644 --- a/examples/operator-quickstart/02-Demo.ipynb +++ b/examples/operator-quickstart/02-Demo.ipynb @@ -113,72 +113,71 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Update the feature store definitions for the tutorial." + "Update the feature store definitions for the tutorial and load data from feature views into the online store, beginning from either the previous materialize or materialize-incremental end date, or the beginning of time.\n", + "\n", + "We'll do this by using the CronJob created by the operator which, by default, will execute the following commands when run -\n", + " - feast apply\n", + " - feast materialize-incremental $(date -u +'%Y-%m-%dT%H:%M:%S')\n", + "\n", + "Before we run the Job, let's ensure our FeatureStore CR is configured accordingly." ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "/opt/app-root/lib64/python3.11/site-packages/feast/feature_view.py:48: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity '__dummy'.\n", - " DUMMY_ENTITY = Entity(\n", - "No project found in the repository. Using project name credit_scoring_local defined in feature_store.yaml\n", - "Applying changes for project credit_scoring_local\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/feature_store.py:579: RuntimeWarning: On demand feature view is an experimental feature. This API is stable, but the functionality does not scale well for offline retrieval\n", - " warnings.warn(\n", - "Deploying infrastructure for \u001b[1m\u001b[32mzipcode_features\u001b[0m\n", - "Deploying infrastructure for \u001b[1m\u001b[32mcredit_history\u001b[0m\n" + "[\"feast apply\",\"feast materialize-incremental $(date -u +'%Y-%m-%dT%H:%M:%S')\"]" ] } ], "source": [ - "!kubectl exec deploy/feast-example -itc online -- feast apply" + "!kubectl get feast/example -o jsonpath='{.status.applied.cronJob.containerConfigs.commands}'" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Load data from feature views into the online store, beginning from either the previous materialize or materialize-incremental end date, or the beginning of time." + "Now let's run a Job from the existing CronJob, wait for its completion, and then check the logs." ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 2, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "/opt/app-root/lib64/python3.11/site-packages/feast/feature_view.py:48: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity '__dummy'.\n", - " DUMMY_ENTITY = Entity(\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/entity.py:173: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity '__dummy'.\n", - " entity = cls(\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/entity.py:173: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity 'dob_ssn'.\n", - " entity = cls(\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/entity.py:173: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity 'zipcode'.\n", - " entity = cls(\n", - "Materializing \u001b[1m\u001b[32m2\u001b[0m feature views to \u001b[1m\u001b[32m2025-02-20 21:23:35+00:00\u001b[0m into the \u001b[1m\u001b[32mredis\u001b[0m online store.\n", + "job.batch/feast-example-apply created\n", + "job.batch/feast-example-apply condition met\n", + "Defaulted container \"online\" out of: online, feast-init (init)\n", + "No project found in the repository. Using project name credit_scoring_local defined in feature_store.yaml\n", + "Applying changes for project credit_scoring_local\n", + "/opt/app-root/src/sdk/python/feast/feature_store.py:581: RuntimeWarning: On demand feature view is an experimental feature. This API is stable, but the functionality does not scale well for offline retrieval\n", + " warnings.warn(\n", + "Deploying infrastructure for zipcode_features\n", + "Deploying infrastructure for credit_history\n", + "Defaulted container \"online\" out of: online, feast-init (init)\n", + "Materializing \u001b[1m\u001b[32m2\u001b[0m feature views to \u001b[1m\u001b[32m2025-04-04 16:25:12+00:00\u001b[0m into the \u001b[1m\u001b[32mredis\u001b[0m online store.\n", "\n", - "\u001b[1m\u001b[32mzipcode_features\u001b[0m from \u001b[1m\u001b[32m2015-02-23 21:24:12+00:00\u001b[0m to \u001b[1m\u001b[32m2025-02-20 21:23:35+00:00\u001b[0m:\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/entity.py:173: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity 'zipcode'.\n", - " entity = cls(\n", - "100%|███████████████████████████████████████████████████████| 28844/28844 [00:28<00:00, 1023.99it/s]\n", - "\u001b[1m\u001b[32mcredit_history\u001b[0m from \u001b[1m\u001b[32m2024-11-22 21:24:43+00:00\u001b[0m to \u001b[1m\u001b[32m2025-02-20 21:23:35+00:00\u001b[0m:\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/entity.py:173: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity 'dob_ssn'.\n", - " entity = cls(\n", + "\u001b[1m\u001b[32mcredit_history\u001b[0m from \u001b[1m\u001b[32m2025-04-04 15:01:55+00:00\u001b[0m to \u001b[1m\u001b[32m2025-04-04 16:25:12+00:00\u001b[0m:\n", + "0it [00:00, ?it/s]\n", + "\u001b[1m\u001b[32mzipcode_features\u001b[0m from \u001b[1m\u001b[32m2025-04-04 15:01:55+00:00\u001b[0m to \u001b[1m\u001b[32m2025-04-04 16:25:12+00:00\u001b[0m:\n", "0it [00:00, ?it/s]\n" ] } ], "source": [ - "!kubectl exec deploy/feast-example -itc online -- bash -c 'feast materialize-incremental $(date -u +\"%Y-%m-%dT%H:%M:%S\")'" + "!kubectl create job --from=cronjob/feast-example feast-example-apply\n", + "!kubectl wait --for=condition=complete --timeout=8m job/feast-example-apply\n", + "!kubectl logs job/feast-example-apply --all-containers=true" ] }, { @@ -197,52 +196,22 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "/opt/app-root/lib64/python3.11/site-packages/feast/feature_view.py:48: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity '__dummy'.\n", - " DUMMY_ENTITY = Entity(\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/entity.py:173: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity '__dummy'.\n", - " entity = cls(\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/entity.py:173: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity 'dob_ssn'.\n", - " entity = cls(\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/entity.py:173: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity 'zipcode'.\n", - " entity = cls(\n", "NAME DESCRIPTION TAGS OWNER\n", "credit_scoring_local {}\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/feature_view.py:48: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity '__dummy'.\n", - " DUMMY_ENTITY = Entity(\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/entity.py:173: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity '__dummy'.\n", - " entity = cls(\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/entity.py:173: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity 'dob_ssn'.\n", - " entity = cls(\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/entity.py:173: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity 'zipcode'.\n", - " entity = cls(\n", "NAME ENTITIES TYPE\n", - "zipcode_features {'zipcode'} FeatureView\n", "credit_history {'dob_ssn'} FeatureView\n", + "zipcode_features {'zipcode'} FeatureView\n", "total_debt_calc {'dob_ssn'} OnDemandFeatureView\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/feature_view.py:48: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity '__dummy'.\n", - " DUMMY_ENTITY = Entity(\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/entity.py:173: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity '__dummy'.\n", - " entity = cls(\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/entity.py:173: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity 'dob_ssn'.\n", - " entity = cls(\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/entity.py:173: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity 'zipcode'.\n", - " entity = cls(\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/entity.py:173: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity '__dummy'.\n", - " entity = cls(\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/entity.py:173: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity 'dob_ssn'.\n", - " entity = cls(\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/entity.py:173: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity 'zipcode'.\n", - " entity = cls(\n", "NAME DESCRIPTION TYPE\n", - "dob_ssn Date of birth and last four digits of social security number ValueType.STRING\n", - "zipcode ValueType.INT64\n" + "zipcode ValueType.INT64\n", + "dob_ssn Date of birth and last four digits of social security number ValueType.STRING\n" ] } ], @@ -263,56 +232,112 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Install the required packages." + "Install the required packages, then train and test the model.\n", + "\n", + "We'll do this by leveraging the same Operator created CronJob. We'll modify it to run the following commands for us -\n", + " - pip install -r ../requirements.txt\n", + " - cd ../ && python run.py" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ + "featurestore.feast.dev/example patched\n" + ] + } + ], + "source": [ + "!kubectl patch feast/example --patch '{\"spec\":{\"cronJob\":{\"containerConfigs\":{\"commands\":[\"pip install -r ../requirements.txt\",\"cd ../ && python run.py\"]}}}}' --type=merge" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Again, before we run the Job, let's ensure our FeatureStore CR is configured accordingly." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[\"pip install -r ../requirements.txt\",\"cd ../ \\u0026\\u0026 python run.py\"]" + ] + } + ], + "source": [ + "!kubectl get feast/example -o jsonpath='{.status.applied.cronJob.containerConfigs.commands}'" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now let's run another Job from the modified CronJob, wait for its completion, and then check the logs.\n", + "The completed Job logs should show all necessary python installs and end with \"Loan rejected!\"." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "job.batch/feast-example-demo created\n", + "job.batch/feast-example-demo condition met\n", + "Defaulted container \"online\" out of: online, feast-init (init)\n", "Collecting streamlit==1.42.0 (from -r ../requirements.txt (line 1))\n", " Obtaining dependency information for streamlit==1.42.0 from https://files.pythonhosted.org/packages/ad/dc/69068179e09488d0833a970d06e8bf40e35669a7bddb8a3caadc13b7dff4/streamlit-1.42.0-py2.py3-none-any.whl.metadata\n", " Downloading streamlit-1.42.0-py2.py3-none-any.whl.metadata (8.9 kB)\n", "Collecting shap (from -r ../requirements.txt (line 2))\n", - " Obtaining dependency information for shap from https://files.pythonhosted.org/packages/06/6a/09e3cb9864118337c0f3c2a0dc5add6b642e9f672665062e186d67ba992d/shap-0.46.0-cp311-cp311-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata\n", - " Downloading shap-0.46.0-cp311-cp311-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (24 kB)\n", + " Obtaining dependency information for shap from https://files.pythonhosted.org/packages/2a/c2/aa91dbb9cc8eee20f5bd245fd8fe27fc45fba786c3e04d35ed510f111b17/shap-0.47.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata\n", + " Downloading shap-0.47.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (25 kB)\n", "Requirement already satisfied: pandas in /opt/app-root/lib64/python3.11/site-packages (from -r ../requirements.txt (line 3)) (2.2.3)\n", "Collecting scikit-learn (from -r ../requirements.txt (line 4))\n", " Obtaining dependency information for scikit-learn from https://files.pythonhosted.org/packages/a8/f3/62fc9a5a659bb58a03cdd7e258956a5824bdc9b4bb3c5d932f55880be569/scikit_learn-1.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata\n", " Downloading scikit_learn-1.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (18 kB)\n", "Collecting matplotlib (from -r ../requirements.txt (line 5))\n", - " Obtaining dependency information for matplotlib from https://files.pythonhosted.org/packages/b2/7d/2d873209536b9ee17340754118a2a17988bc18981b5b56e6715ee07373ac/matplotlib-3.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata\n", - " Downloading matplotlib-3.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (11 kB)\n", + " Obtaining dependency information for matplotlib from https://files.pythonhosted.org/packages/40/b8/53fa08a5eaf78d3a7213fd6da1feec4bae14a81d9805e567013811ff0e85/matplotlib-3.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata\n", + " Downloading matplotlib-3.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (11 kB)\n", "Collecting altair<6,>=4.0 (from streamlit==1.42.0->-r ../requirements.txt (line 1))\n", " Obtaining dependency information for altair<6,>=4.0 from https://files.pythonhosted.org/packages/aa/f3/0b6ced594e51cc95d8c1fc1640d3623770d01e4969d29c0bd09945fafefa/altair-5.5.0-py3-none-any.whl.metadata\n", " Downloading altair-5.5.0-py3-none-any.whl.metadata (11 kB)\n", "Collecting blinker<2,>=1.0.0 (from streamlit==1.42.0->-r ../requirements.txt (line 1))\n", " Obtaining dependency information for blinker<2,>=1.0.0 from https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl.metadata\n", " Downloading blinker-1.9.0-py3-none-any.whl.metadata (1.6 kB)\n", - "Requirement already satisfied: cachetools<6,>=4.0 in /opt/app-root/lib64/python3.11/site-packages (from streamlit==1.42.0->-r ../requirements.txt (line 1)) (5.5.1)\n", + "Requirement already satisfied: cachetools<6,>=4.0 in /opt/app-root/lib64/python3.11/site-packages (from streamlit==1.42.0->-r ../requirements.txt (line 1)) (5.5.2)\n", "Requirement already satisfied: click<9,>=7.0 in /opt/app-root/lib64/python3.11/site-packages (from streamlit==1.42.0->-r ../requirements.txt (line 1)) (8.1.8)\n", "Requirement already satisfied: numpy<3,>=1.23 in /opt/app-root/lib64/python3.11/site-packages (from streamlit==1.42.0->-r ../requirements.txt (line 1)) (1.26.4)\n", "Requirement already satisfied: packaging<25,>=20 in /opt/app-root/lib64/python3.11/site-packages (from streamlit==1.42.0->-r ../requirements.txt (line 1)) (24.2)\n", "Collecting pillow<12,>=7.1.0 (from streamlit==1.42.0->-r ../requirements.txt (line 1))\n", " Obtaining dependency information for pillow<12,>=7.1.0 from https://files.pythonhosted.org/packages/48/a4/fbfe9d5581d7b111b28f1d8c2762dee92e9821bb209af9fa83c940e507a0/pillow-11.1.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata\n", " Downloading pillow-11.1.0-cp311-cp311-manylinux_2_28_x86_64.whl.metadata (9.1 kB)\n", - "Requirement already satisfied: protobuf<6,>=3.20 in /opt/app-root/lib64/python3.11/site-packages (from streamlit==1.42.0->-r ../requirements.txt (line 1)) (5.29.3)\n", + "Requirement already satisfied: protobuf<6,>=3.20 in /opt/app-root/lib64/python3.11/site-packages (from streamlit==1.42.0->-r ../requirements.txt (line 1)) (5.29.4)\n", "Requirement already satisfied: pyarrow>=7.0 in /opt/app-root/lib64/python3.11/site-packages (from streamlit==1.42.0->-r ../requirements.txt (line 1)) (17.0.0)\n", "Requirement already satisfied: requests<3,>=2.27 in /opt/app-root/lib64/python3.11/site-packages (from streamlit==1.42.0->-r ../requirements.txt (line 1)) (2.32.3)\n", "Requirement already satisfied: rich<14,>=10.14.0 in /opt/app-root/lib64/python3.11/site-packages (from streamlit==1.42.0->-r ../requirements.txt (line 1)) (13.9.4)\n", "Requirement already satisfied: tenacity<10,>=8.1.0 in /opt/app-root/lib64/python3.11/site-packages (from streamlit==1.42.0->-r ../requirements.txt (line 1)) (8.5.0)\n", "Requirement already satisfied: toml<2,>=0.10.1 in /opt/app-root/lib64/python3.11/site-packages (from streamlit==1.42.0->-r ../requirements.txt (line 1)) (0.10.2)\n", - "Requirement already satisfied: typing-extensions<5,>=4.4.0 in /opt/app-root/lib64/python3.11/site-packages (from streamlit==1.42.0->-r ../requirements.txt (line 1)) (4.12.2)\n", + "Requirement already satisfied: typing-extensions<5,>=4.4.0 in /opt/app-root/lib64/python3.11/site-packages (from streamlit==1.42.0->-r ../requirements.txt (line 1)) (4.13.0)\n", "Collecting watchdog<7,>=2.1.5 (from streamlit==1.42.0->-r ../requirements.txt (line 1))\n", " Obtaining dependency information for watchdog<7,>=2.1.5 from https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl.metadata\n", " Downloading watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl.metadata (44 kB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m44.3/44.3 kB\u001b[0m \u001b[31m13.2 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", - "\u001b[?25hCollecting gitpython!=3.1.19,<4,>=3.0.7 (from streamlit==1.42.0->-r ../requirements.txt (line 1))\n", + " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 44.3/44.3 kB 11.2 MB/s eta 0:00:00\n", + "Collecting gitpython!=3.1.19,<4,>=3.0.7 (from streamlit==1.42.0->-r ../requirements.txt (line 1))\n", " Obtaining dependency information for gitpython!=3.1.19,<4,>=3.0.7 from https://files.pythonhosted.org/packages/1d/9a/4114a9057db2f1462d5c8f8390ab7383925fe1ac012eaa42402ad65c2963/GitPython-3.1.44-py3-none-any.whl.metadata\n", " Downloading GitPython-3.1.44-py3-none-any.whl.metadata (13 kB)\n", "Collecting pydeck<1,>=0.8.0b4 (from streamlit==1.42.0->-r ../requirements.txt (line 1))\n", @@ -324,24 +349,24 @@ "Collecting scipy (from shap->-r ../requirements.txt (line 2))\n", " Obtaining dependency information for scipy from https://files.pythonhosted.org/packages/32/ea/564bacc26b676c06a00266a3f25fdfe91a9d9a2532ccea7ce6dd394541bc/scipy-1.15.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata\n", " Downloading scipy-1.15.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (61 kB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m62.0/62.0 kB\u001b[0m \u001b[31m8.7 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", - "\u001b[?25hRequirement already satisfied: tqdm>=4.27.0 in /opt/app-root/lib64/python3.11/site-packages (from shap->-r ../requirements.txt (line 2)) (4.67.1)\n", + " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 62.0/62.0 kB 7.6 MB/s eta 0:00:00\n", + "Requirement already satisfied: tqdm>=4.27.0 in /opt/app-root/lib64/python3.11/site-packages (from shap->-r ../requirements.txt (line 2)) (4.67.1)\n", "Collecting slicer==0.0.8 (from shap->-r ../requirements.txt (line 2))\n", " Obtaining dependency information for slicer==0.0.8 from https://files.pythonhosted.org/packages/63/81/9ef641ff4e12cbcca30e54e72fb0951a2ba195d0cda0ba4100e532d929db/slicer-0.0.8-py3-none-any.whl.metadata\n", " Downloading slicer-0.0.8-py3-none-any.whl.metadata (4.0 kB)\n", - "Collecting numba (from shap->-r ../requirements.txt (line 2))\n", - " Obtaining dependency information for numba from https://files.pythonhosted.org/packages/14/91/18b9f64b34ff318a14d072251480547f89ebfb864b2b7168e5dc5f64f502/numba-0.61.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata\n", + "Collecting numba>=0.54 (from shap->-r ../requirements.txt (line 2))\n", + " Obtaining dependency information for numba>=0.54 from https://files.pythonhosted.org/packages/14/91/18b9f64b34ff318a14d072251480547f89ebfb864b2b7168e5dc5f64f502/numba-0.61.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata\n", " Downloading numba-0.61.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl.metadata (2.8 kB)\n", "Requirement already satisfied: cloudpickle in /opt/app-root/lib64/python3.11/site-packages (from shap->-r ../requirements.txt (line 2)) (3.1.1)\n", "Requirement already satisfied: python-dateutil>=2.8.2 in /opt/app-root/lib64/python3.11/site-packages (from pandas->-r ../requirements.txt (line 3)) (2.9.0.post0)\n", - "Requirement already satisfied: pytz>=2020.1 in /opt/app-root/lib64/python3.11/site-packages (from pandas->-r ../requirements.txt (line 3)) (2025.1)\n", - "Requirement already satisfied: tzdata>=2022.7 in /opt/app-root/lib64/python3.11/site-packages (from pandas->-r ../requirements.txt (line 3)) (2025.1)\n", + "Requirement already satisfied: pytz>=2020.1 in /opt/app-root/lib64/python3.11/site-packages (from pandas->-r ../requirements.txt (line 3)) (2025.2)\n", + "Requirement already satisfied: tzdata>=2022.7 in /opt/app-root/lib64/python3.11/site-packages (from pandas->-r ../requirements.txt (line 3)) (2025.2)\n", "Collecting joblib>=1.2.0 (from scikit-learn->-r ../requirements.txt (line 4))\n", " Obtaining dependency information for joblib>=1.2.0 from https://files.pythonhosted.org/packages/91/29/df4b9b42f2be0b623cbd5e2140cafcaa2bef0759a00b7b70104dcfe2fb51/joblib-1.4.2-py3-none-any.whl.metadata\n", " Downloading joblib-1.4.2-py3-none-any.whl.metadata (5.4 kB)\n", "Collecting threadpoolctl>=3.1.0 (from scikit-learn->-r ../requirements.txt (line 4))\n", - " Obtaining dependency information for threadpoolctl>=3.1.0 from https://files.pythonhosted.org/packages/4b/2c/ffbf7a134b9ab11a67b0cf0726453cedd9c5043a4fe7a35d1cefa9a1bcfb/threadpoolctl-3.5.0-py3-none-any.whl.metadata\n", - " Downloading threadpoolctl-3.5.0-py3-none-any.whl.metadata (13 kB)\n", + " Obtaining dependency information for threadpoolctl>=3.1.0 from https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl.metadata\n", + " Downloading threadpoolctl-3.6.0-py3-none-any.whl.metadata (13 kB)\n", "Collecting contourpy>=1.0.1 (from matplotlib->-r ../requirements.txt (line 5))\n", " Obtaining dependency information for contourpy>=1.0.1 from https://files.pythonhosted.org/packages/85/fc/7fa5d17daf77306840a4e84668a48ddff09e6bc09ba4e37e85ffc8e4faa3/contourpy-1.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata\n", " Downloading contourpy-1.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (5.4 kB)\n", @@ -349,23 +374,26 @@ " Obtaining dependency information for cycler>=0.10 from https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl.metadata\n", " Downloading cycler-0.12.1-py3-none-any.whl.metadata (3.8 kB)\n", "Collecting fonttools>=4.22.0 (from matplotlib->-r ../requirements.txt (line 5))\n", - " Obtaining dependency information for fonttools>=4.22.0 from https://files.pythonhosted.org/packages/28/e9/47c02d5a7027e8ed841ab6a10ca00c93dadd5f16742f1af1fa3f9978adf4/fonttools-4.56.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata\n", - " Downloading fonttools-4.56.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (101 kB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m101.9/101.9 kB\u001b[0m \u001b[31m9.2 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", - "\u001b[?25hCollecting kiwisolver>=1.3.1 (from matplotlib->-r ../requirements.txt (line 5))\n", + " Obtaining dependency information for fonttools>=4.22.0 from https://files.pythonhosted.org/packages/41/b8/d5933559303a4ab18c799105f4c91ee0318cc95db4a2a09e300116625e7a/fonttools-4.57.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata\n", + " Downloading fonttools-4.57.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (102 kB)\n", + " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 102.5/102.5 kB 7.1 MB/s eta 0:00:00\n", + "Collecting kiwisolver>=1.3.1 (from matplotlib->-r ../requirements.txt (line 5))\n", " Obtaining dependency information for kiwisolver>=1.3.1 from https://files.pythonhosted.org/packages/3a/97/5edbed69a9d0caa2e4aa616ae7df8127e10f6586940aa683a496c2c280b9/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata\n", " Downloading kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (6.2 kB)\n", "Collecting pyparsing>=2.3.1 (from matplotlib->-r ../requirements.txt (line 5))\n", - " Obtaining dependency information for pyparsing>=2.3.1 from https://files.pythonhosted.org/packages/1c/a7/c8a2d361bf89c0d9577c934ebb7421b25dc84bf3a8e3ac0a40aed9acc547/pyparsing-3.2.1-py3-none-any.whl.metadata\n", - " Downloading pyparsing-3.2.1-py3-none-any.whl.metadata (5.0 kB)\n", - "Requirement already satisfied: jinja2 in /opt/app-root/lib64/python3.11/site-packages (from altair<6,>=4.0->streamlit==1.42.0->-r ../requirements.txt (line 1)) (3.1.5)\n", + " Obtaining dependency information for pyparsing>=2.3.1 from https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl.metadata\n", + " Downloading pyparsing-3.2.3-py3-none-any.whl.metadata (5.0 kB)\n", + "Requirement already satisfied: jinja2 in /opt/app-root/lib64/python3.11/site-packages (from altair<6,>=4.0->streamlit==1.42.0->-r ../requirements.txt (line 1)) (3.1.6)\n", "Requirement already satisfied: jsonschema>=3.0 in /opt/app-root/lib64/python3.11/site-packages (from altair<6,>=4.0->streamlit==1.42.0->-r ../requirements.txt (line 1)) (4.23.0)\n", "Collecting narwhals>=1.14.2 (from altair<6,>=4.0->streamlit==1.42.0->-r ../requirements.txt (line 1))\n", - " Obtaining dependency information for narwhals>=1.14.2 from https://files.pythonhosted.org/packages/ed/ea/dc14822a0a75e027562f081eb638417b1b7845e1e01dd85c5b6573ebf1b2/narwhals-1.27.1-py3-none-any.whl.metadata\n", - " Downloading narwhals-1.27.1-py3-none-any.whl.metadata (10 kB)\n", + " Obtaining dependency information for narwhals>=1.14.2 from https://files.pythonhosted.org/packages/41/c1/e9bc6b67c774e7c1f939c91ea535f18f7644fedc61b20d6baa861ad52b34/narwhals-1.33.0-py3-none-any.whl.metadata\n", + " Downloading narwhals-1.33.0-py3-none-any.whl.metadata (9.2 kB)\n", "Collecting gitdb<5,>=4.0.1 (from gitpython!=3.1.19,<4,>=3.0.7->streamlit==1.42.0->-r ../requirements.txt (line 1))\n", " Obtaining dependency information for gitdb<5,>=4.0.1 from https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl.metadata\n", " Downloading gitdb-4.0.12-py3-none-any.whl.metadata (1.2 kB)\n", + "Collecting llvmlite<0.45,>=0.44.0dev0 (from numba>=0.54->shap->-r ../requirements.txt (line 2))\n", + " Obtaining dependency information for llvmlite<0.45,>=0.44.0dev0 from https://files.pythonhosted.org/packages/99/fe/d030f1849ebb1f394bb3f7adad5e729b634fb100515594aca25c354ffc62/llvmlite-0.44.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata\n", + " Downloading llvmlite-0.44.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (4.8 kB)\n", "Requirement already satisfied: six>=1.5 in /opt/app-root/lib64/python3.11/site-packages (from python-dateutil>=2.8.2->pandas->-r ../requirements.txt (line 3)) (1.17.0)\n", "Requirement already satisfied: charset-normalizer<4,>=2 in /opt/app-root/lib64/python3.11/site-packages (from requests<3,>=2.27->streamlit==1.42.0->-r ../requirements.txt (line 1)) (3.4.1)\n", "Requirement already satisfied: idna<4,>=2.5 in /opt/app-root/lib64/python3.11/site-packages (from requests<3,>=2.27->streamlit==1.42.0->-r ../requirements.txt (line 1)) (3.10)\n", @@ -373,111 +401,74 @@ "Requirement already satisfied: certifi>=2017.4.17 in /opt/app-root/lib64/python3.11/site-packages (from requests<3,>=2.27->streamlit==1.42.0->-r ../requirements.txt (line 1)) (2025.1.31)\n", "Requirement already satisfied: markdown-it-py>=2.2.0 in /opt/app-root/lib64/python3.11/site-packages (from rich<14,>=10.14.0->streamlit==1.42.0->-r ../requirements.txt (line 1)) (3.0.0)\n", "Requirement already satisfied: pygments<3.0.0,>=2.13.0 in /opt/app-root/lib64/python3.11/site-packages (from rich<14,>=10.14.0->streamlit==1.42.0->-r ../requirements.txt (line 1)) (2.19.1)\n", - "Collecting llvmlite<0.45,>=0.44.0dev0 (from numba->shap->-r ../requirements.txt (line 2))\n", - " Obtaining dependency information for llvmlite<0.45,>=0.44.0dev0 from https://files.pythonhosted.org/packages/99/fe/d030f1849ebb1f394bb3f7adad5e729b634fb100515594aca25c354ffc62/llvmlite-0.44.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata\n", - " Downloading llvmlite-0.44.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (4.8 kB)\n", "Collecting smmap<6,>=3.0.1 (from gitdb<5,>=4.0.1->gitpython!=3.1.19,<4,>=3.0.7->streamlit==1.42.0->-r ../requirements.txt (line 1))\n", " Obtaining dependency information for smmap<6,>=3.0.1 from https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl.metadata\n", " Downloading smmap-5.0.2-py3-none-any.whl.metadata (4.3 kB)\n", "Requirement already satisfied: MarkupSafe>=2.0 in /opt/app-root/lib64/python3.11/site-packages (from jinja2->altair<6,>=4.0->streamlit==1.42.0->-r ../requirements.txt (line 1)) (3.0.2)\n", - "Requirement already satisfied: attrs>=22.2.0 in /opt/app-root/lib64/python3.11/site-packages (from jsonschema>=3.0->altair<6,>=4.0->streamlit==1.42.0->-r ../requirements.txt (line 1)) (25.1.0)\n", + "Requirement already satisfied: attrs>=22.2.0 in /opt/app-root/lib64/python3.11/site-packages (from jsonschema>=3.0->altair<6,>=4.0->streamlit==1.42.0->-r ../requirements.txt (line 1)) (25.3.0)\n", "Requirement already satisfied: jsonschema-specifications>=2023.03.6 in /opt/app-root/lib64/python3.11/site-packages (from jsonschema>=3.0->altair<6,>=4.0->streamlit==1.42.0->-r ../requirements.txt (line 1)) (2024.10.1)\n", "Requirement already satisfied: referencing>=0.28.4 in /opt/app-root/lib64/python3.11/site-packages (from jsonschema>=3.0->altair<6,>=4.0->streamlit==1.42.0->-r ../requirements.txt (line 1)) (0.36.2)\n", - "Requirement already satisfied: rpds-py>=0.7.1 in /opt/app-root/lib64/python3.11/site-packages (from jsonschema>=3.0->altair<6,>=4.0->streamlit==1.42.0->-r ../requirements.txt (line 1)) (0.22.3)\n", + "Requirement already satisfied: rpds-py>=0.7.1 in /opt/app-root/lib64/python3.11/site-packages (from jsonschema>=3.0->altair<6,>=4.0->streamlit==1.42.0->-r ../requirements.txt (line 1)) (0.24.0)\n", "Requirement already satisfied: mdurl~=0.1 in /opt/app-root/lib64/python3.11/site-packages (from markdown-it-py>=2.2.0->rich<14,>=10.14.0->streamlit==1.42.0->-r ../requirements.txt (line 1)) (0.1.2)\n", "Downloading streamlit-1.42.0-py2.py3-none-any.whl (9.6 MB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m9.6/9.6 MB\u001b[0m \u001b[31m5.9 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m00:01\u001b[0m00:01\u001b[0mm\n", - "\u001b[?25hDownloading shap-0.46.0-cp311-cp311-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl (540 kB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m540.2/540.2 kB\u001b[0m \u001b[31m7.0 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0ma \u001b[36m0:00:01\u001b[0m\n", - "\u001b[?25hDownloading slicer-0.0.8-py3-none-any.whl (15 kB)\n", + " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 9.6/9.6 MB 11.2 MB/s eta 0:00:00\n", + "Downloading shap-0.47.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (972 kB)\n", + " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 972.6/972.6 kB 13.8 MB/s eta 0:00:00\n", + "Downloading slicer-0.0.8-py3-none-any.whl (15 kB)\n", "Downloading scikit_learn-1.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (13.5 MB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m13.5/13.5 MB\u001b[0m \u001b[31m6.0 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m00:01\u001b[0m00:01\u001b[0m\n", - "\u001b[?25hDownloading matplotlib-3.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (8.6 MB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m8.6/8.6 MB\u001b[0m \u001b[31m3.1 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m00:01\u001b[0m00:01\u001b[0m\n", - "\u001b[?25hDownloading altair-5.5.0-py3-none-any.whl (731 kB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m731.2/731.2 kB\u001b[0m \u001b[31m3.3 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0ma \u001b[36m0:00:01\u001b[0m\n", - "\u001b[?25hDownloading blinker-1.9.0-py3-none-any.whl (8.5 kB)\n", + " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 13.5/13.5 MB 5.7 MB/s eta 0:00:00\n", + "Downloading matplotlib-3.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (8.6 MB)\n", + " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 8.6/8.6 MB 3.0 MB/s eta 0:00:00\n", + "Downloading altair-5.5.0-py3-none-any.whl (731 kB)\n", + " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 731.2/731.2 kB 5.8 MB/s eta 0:00:00\n", + "Downloading blinker-1.9.0-py3-none-any.whl (8.5 kB)\n", "Downloading contourpy-1.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (326 kB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m326.2/326.2 kB\u001b[0m \u001b[31m2.7 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0ma \u001b[36m0:00:01\u001b[0m\n", - "\u001b[?25hDownloading cycler-0.12.1-py3-none-any.whl (8.3 kB)\n", - "Downloading fonttools-4.56.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (4.9 MB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m4.9/4.9 MB\u001b[0m \u001b[31m3.3 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0ma \u001b[36m0:00:01\u001b[0m\n", - "\u001b[?25hDownloading GitPython-3.1.44-py3-none-any.whl (207 kB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m207.6/207.6 kB\u001b[0m \u001b[31m15.4 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", - "\u001b[?25hDownloading joblib-1.4.2-py3-none-any.whl (301 kB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m301.8/301.8 kB\u001b[0m \u001b[31m15.8 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", - "\u001b[?25hDownloading kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.4 MB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m1.4/1.4 MB\u001b[0m \u001b[31m6.7 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0ma \u001b[36m0:00:01\u001b[0m\n", - "\u001b[?25hDownloading pillow-11.1.0-cp311-cp311-manylinux_2_28_x86_64.whl (4.5 MB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m4.5/4.5 MB\u001b[0m \u001b[31m4.6 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0ma \u001b[36m0:00:01\u001b[0mm\n", - "\u001b[?25hDownloading pydeck-0.9.1-py2.py3-none-any.whl (6.9 MB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m6.9/6.9 MB\u001b[0m \u001b[31m4.5 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m00:01\u001b[0m00:01\u001b[0mm\n", - "\u001b[?25hDownloading pyparsing-3.2.1-py3-none-any.whl (107 kB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m107.7/107.7 kB\u001b[0m \u001b[31m4.6 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", - "\u001b[?25hDownloading scipy-1.15.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (37.6 MB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m37.6/37.6 MB\u001b[0m \u001b[31m4.3 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m00:01\u001b[0m00:01\u001b[0m\n", - "\u001b[?25hDownloading threadpoolctl-3.5.0-py3-none-any.whl (18 kB)\n", + " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 326.2/326.2 kB 5.6 MB/s eta 0:00:00\n", + "Downloading cycler-0.12.1-py3-none-any.whl (8.3 kB)\n", + "Downloading fonttools-4.57.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (4.9 MB)\n", + " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.9/4.9 MB 6.8 MB/s eta 0:00:00\n", + "Downloading GitPython-3.1.44-py3-none-any.whl (207 kB)\n", + " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 207.6/207.6 kB 21.0 MB/s eta 0:00:00\n", + "Downloading joblib-1.4.2-py3-none-any.whl (301 kB)\n", + " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 301.8/301.8 kB 15.0 MB/s eta 0:00:00\n", + "Downloading kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.4 MB)\n", + " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.4/1.4 MB 6.8 MB/s eta 0:00:00\n", + "Downloading numba-0.61.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (3.8 MB)\n", + " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 3.8/3.8 MB 9.3 MB/s eta 0:00:00\n", + "Downloading pillow-11.1.0-cp311-cp311-manylinux_2_28_x86_64.whl (4.5 MB)\n", + " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.5/4.5 MB 9.8 MB/s eta 0:00:00\n", + "Downloading pydeck-0.9.1-py2.py3-none-any.whl (6.9 MB)\n", + " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 6.9/6.9 MB 9.4 MB/s eta 0:00:00\n", + "Downloading pyparsing-3.2.3-py3-none-any.whl (111 kB)\n", + " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 111.1/111.1 kB 12.0 MB/s eta 0:00:00\n", + "Downloading scipy-1.15.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (37.6 MB)\n", + " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 37.6/37.6 MB 3.5 MB/s eta 0:00:00\n", + "Downloading threadpoolctl-3.6.0-py3-none-any.whl (18 kB)\n", "Downloading tornado-6.4.2-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl (437 kB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m437.2/437.2 kB\u001b[0m \u001b[31m14.1 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", - "\u001b[?25hDownloading watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl (79 kB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m79.1/79.1 kB\u001b[0m \u001b[31m14.6 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", - "\u001b[?25hDownloading numba-0.61.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (3.8 MB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m3.8/3.8 MB\u001b[0m \u001b[31m2.2 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m00:01\u001b[0m00:01\u001b[0m\n", - "\u001b[?25hDownloading gitdb-4.0.12-py3-none-any.whl (62 kB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m62.8/62.8 kB\u001b[0m \u001b[31m6.3 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", - "\u001b[?25hDownloading llvmlite-0.44.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (42.4 MB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m42.4/42.4 MB\u001b[0m \u001b[31m2.5 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m00:01\u001b[0m00:01\u001b[0m\n", - "\u001b[?25hDownloading narwhals-1.27.1-py3-none-any.whl (308 kB)\n", - "\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m308.8/308.8 kB\u001b[0m \u001b[31m3.4 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0ma \u001b[36m0:00:01\u001b[0m\n", - "\u001b[?25hDownloading smmap-5.0.2-py3-none-any.whl (24 kB)\n", + " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 437.2/437.2 kB 8.4 MB/s eta 0:00:00\n", + "Downloading watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl (79 kB)\n", + " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 79.1/79.1 kB 5.8 MB/s eta 0:00:00\n", + "Downloading gitdb-4.0.12-py3-none-any.whl (62 kB)\n", + " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 62.8/62.8 kB 9.3 MB/s eta 0:00:00\n", + "Downloading llvmlite-0.44.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (42.4 MB)\n", + " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 42.4/42.4 MB 7.3 MB/s eta 0:00:00\n", + "Downloading narwhals-1.33.0-py3-none-any.whl (322 kB)\n", + " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 322.8/322.8 kB 5.6 MB/s eta 0:00:00\n", + "Downloading smmap-5.0.2-py3-none-any.whl (24 kB)\n", "Installing collected packages: watchdog, tornado, threadpoolctl, smmap, slicer, scipy, pyparsing, pillow, narwhals, llvmlite, kiwisolver, joblib, fonttools, cycler, contourpy, blinker, scikit-learn, pydeck, numba, matplotlib, gitdb, shap, gitpython, altair, streamlit\n", - "Successfully installed altair-5.5.0 blinker-1.9.0 contourpy-1.3.1 cycler-0.12.1 fonttools-4.56.0 gitdb-4.0.12 gitpython-3.1.44 joblib-1.4.2 kiwisolver-1.4.8 llvmlite-0.44.0 matplotlib-3.10.0 narwhals-1.27.1 numba-0.61.0 pillow-11.1.0 pydeck-0.9.1 pyparsing-3.2.1 scikit-learn-1.6.1 scipy-1.15.2 shap-0.46.0 slicer-0.0.8 smmap-5.0.2 streamlit-1.42.0 threadpoolctl-3.5.0 tornado-6.4.2 watchdog-6.0.0\n", + "Successfully installed altair-5.5.0 blinker-1.9.0 contourpy-1.3.1 cycler-0.12.1 fonttools-4.57.0 gitdb-4.0.12 gitpython-3.1.44 joblib-1.4.2 kiwisolver-1.4.8 llvmlite-0.44.0 matplotlib-3.10.1 narwhals-1.33.0 numba-0.61.0 pillow-11.1.0 pydeck-0.9.1 pyparsing-3.2.3 scikit-learn-1.6.1 scipy-1.15.2 shap-0.47.1 slicer-0.0.8 smmap-5.0.2 streamlit-1.42.0 threadpoolctl-3.6.0 tornado-6.4.2 watchdog-6.0.0\n", "\n", - "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m23.2.1\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m25.0.1\u001b[0m\n", - "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpip install --upgrade pip\u001b[0m\n" - ] - } - ], - "source": [ - "!kubectl exec deploy/feast-example -itc online -- bash -c 'pip install -r ../requirements.txt'" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Train and test the model." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "/opt/app-root/lib64/python3.11/site-packages/feast/feature_view.py:48: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity '__dummy'.\n", - " DUMMY_ENTITY = Entity(\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/entity.py:173: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity '__dummy'.\n", - " entity = cls(\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/entity.py:173: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity 'dob_ssn'.\n", - " entity = cls(\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/entity.py:173: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity 'zipcode'.\n", - " entity = cls(\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/entity.py:173: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity '__dummy'.\n", - " entity = cls(\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/entity.py:173: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity 'dob_ssn'.\n", - " entity = cls(\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/entity.py:173: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity 'zipcode'.\n", - " entity = cls(\n", + "[notice] A new release of pip is available: 23.2.1 -> 25.0.1\n", + "[notice] To update, run: pip install --upgrade pip\n", + "Defaulted container \"online\" out of: online, feast-init (init)\n", "Loan rejected!\n" ] } ], "source": [ - "!kubectl exec deploy/feast-example -itc online -- bash -c 'cd ../ && python run.py'" + "!kubectl create job --from=cronjob/feast-example feast-example-demo\n", + "!kubectl wait --for=condition=complete --timeout=8m job/feast-example-demo\n", + "!kubectl logs job/feast-example-demo --all-containers=true" ] }, { @@ -521,113 +512,9 @@ "\u001b[34m\u001b[1m You can now view your Streamlit app in your browser.\u001b[0m\n", "\u001b[0m\n", "\u001b[34m Local URL: \u001b[0m\u001b[1mhttp://localhost:8501\u001b[0m\n", - "\u001b[34m Network URL: \u001b[0m\u001b[1mhttp://10.42.0.8:8501\u001b[0m\n", + "\u001b[34m Network URL: \u001b[0m\u001b[1mhttp://10.42.0.11:8501\u001b[0m\n", "\u001b[34m External URL: \u001b[0m\u001b[1mhttp://23.112.66.217:8501\u001b[0m\n", - "\u001b[0m\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/feature_view.py:48: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity '__dummy'.\n", - " DUMMY_ENTITY = Entity(\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/entity.py:173: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity '__dummy'.\n", - " entity = cls(\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/entity.py:173: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity 'dob_ssn'.\n", - " entity = cls(\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/entity.py:173: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity 'zipcode'.\n", - " entity = cls(\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/entity.py:173: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity '__dummy'.\n", - " entity = cls(\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/entity.py:173: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity 'dob_ssn'.\n", - " entity = cls(\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/entity.py:173: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity 'zipcode'.\n", - " entity = cls(\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/entity.py:173: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity 'zipcode'.\n", - " entity = cls(\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/entity.py:173: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity 'dob_ssn'.\n", - " entity = cls(\n", - "(1000, 22)\n", - "2025-02-20 21:57:48.314 \n", - "Calling `st.pyplot()` without providing a figure argument has been deprecated\n", - "and will be removed in a later version as it requires the use of Matplotlib's\n", - "global figure object, which is not thread-safe.\n", - "\n", - "To future-proof this code, you should pass in a figure as shown below:\n", - "\n", - "```python\n", - "fig, ax = plt.subplots()\n", - "ax.scatter([1, 2, 3], [1, 2, 3])\n", - "# other plotting actions...\n", - "st.pyplot(fig)\n", - "```\n", - "\n", - "If you have a specific use case that requires this functionality, please let us\n", - "know via [issue on Github](https://github.com/streamlit/streamlit/issues).\n", - "\n", - "2025-02-20 21:57:57.474 \n", - "Calling `st.pyplot()` without providing a figure argument has been deprecated\n", - "and will be removed in a later version as it requires the use of Matplotlib's\n", - "global figure object, which is not thread-safe.\n", - "\n", - "To future-proof this code, you should pass in a figure as shown below:\n", - "\n", - "```python\n", - "fig, ax = plt.subplots()\n", - "ax.scatter([1, 2, 3], [1, 2, 3])\n", - "# other plotting actions...\n", - "st.pyplot(fig)\n", - "```\n", - "\n", - "If you have a specific use case that requires this functionality, please let us\n", - "know via [issue on Github](https://github.com/streamlit/streamlit/issues).\n", - "\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/entity.py:173: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity '__dummy'.\n", - " entity = cls(\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/entity.py:173: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity 'dob_ssn'.\n", - " entity = cls(\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/entity.py:173: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity 'zipcode'.\n", - " entity = cls(\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/entity.py:173: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity '__dummy'.\n", - " entity = cls(\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/entity.py:173: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity 'dob_ssn'.\n", - " entity = cls(\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/entity.py:173: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity 'zipcode'.\n", - " entity = cls(\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/entity.py:173: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity 'zipcode'.\n", - " entity = cls(\n", - "/opt/app-root/lib64/python3.11/site-packages/feast/entity.py:173: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity 'dob_ssn'.\n", - " entity = cls(\n", - "(1000, 22)\n", - "2025-02-20 21:58:34.935 \n", - "Calling `st.pyplot()` without providing a figure argument has been deprecated\n", - "and will be removed in a later version as it requires the use of Matplotlib's\n", - "global figure object, which is not thread-safe.\n", - "\n", - "To future-proof this code, you should pass in a figure as shown below:\n", - "\n", - "```python\n", - "fig, ax = plt.subplots()\n", - "ax.scatter([1, 2, 3], [1, 2, 3])\n", - "# other plotting actions...\n", - "st.pyplot(fig)\n", - "```\n", - "\n", - "If you have a specific use case that requires this functionality, please let us\n", - "know via [issue on Github](https://github.com/streamlit/streamlit/issues).\n", - "\n", - "2025-02-20 21:58:43.709 \n", - "Calling `st.pyplot()` without providing a figure argument has been deprecated\n", - "and will be removed in a later version as it requires the use of Matplotlib's\n", - "global figure object, which is not thread-safe.\n", - "\n", - "To future-proof this code, you should pass in a figure as shown below:\n", - "\n", - "```python\n", - "fig, ax = plt.subplots()\n", - "ax.scatter([1, 2, 3], [1, 2, 3])\n", - "# other plotting actions...\n", - "st.pyplot(fig)\n", - "```\n", - "\n", - "If you have a specific use case that requires this functionality, please let us\n", - "know via [issue on Github](https://github.com/streamlit/streamlit/issues).\n", - "\n" + "\u001b[0m\n" ] } ], diff --git a/examples/operator-quickstart/03-Uninstall.ipynb b/examples/operator-quickstart/03-Uninstall.ipynb index 3abd489dd58..571837d1179 100644 --- a/examples/operator-quickstart/03-Uninstall.ipynb +++ b/examples/operator-quickstart/03-Uninstall.ipynb @@ -18,11 +18,6 @@ "text": [ "secret \"feast-data-stores\" deleted\n", "featurestore.feast.dev \"example\" deleted\n", - "secret \"postgres-secret\" deleted\n", - "deployment.apps \"postgres\" deleted\n", - "service \"postgres\" deleted\n", - "deployment.apps \"redis\" deleted\n", - "service \"redis\" deleted\n", "namespace \"feast-operator-system\" deleted\n", "customresourcedefinition.apiextensions.k8s.io \"featurestores.feast.dev\" deleted\n", "serviceaccount \"feast-operator-controller-manager\" deleted\n", @@ -36,15 +31,20 @@ "clusterrolebinding.rbac.authorization.k8s.io \"feast-operator-manager-rolebinding\" deleted\n", "clusterrolebinding.rbac.authorization.k8s.io \"feast-operator-metrics-auth-rolebinding\" deleted\n", "service \"feast-operator-controller-manager-metrics-service\" deleted\n", - "deployment.apps \"feast-operator-controller-manager\" deleted\n" + "deployment.apps \"feast-operator-controller-manager\" deleted\n", + "secret \"postgres-secret\" deleted\n", + "deployment.apps \"postgres\" deleted\n", + "service \"postgres\" deleted\n", + "deployment.apps \"redis\" deleted\n", + "service \"redis\" deleted\n" ] } ], "source": [ "!kubectl delete -f feast.yaml\n", + "!kubectl delete -f ../../infra/feast-operator/dist/install.yaml\n", "!kubectl delete -f postgres.yaml\n", - "!kubectl delete -f redis.yaml\n", - "!kubectl delete -f ../../infra/feast-operator/dist/install.yaml" + "!kubectl delete -f redis.yaml" ] }, { @@ -56,7 +56,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 5, "metadata": {}, "outputs": [ { @@ -70,13 +70,6 @@ "source": [ "!kubectl get all" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/infra/feast-operator/Makefile b/infra/feast-operator/Makefile index 254d577beae..56286988bed 100644 --- a/infra/feast-operator/Makefile +++ b/infra/feast-operator/Makefile @@ -52,6 +52,7 @@ OPERATOR_SDK_VERSION ?= v1.38.0 # Image URL to use all building/pushing image targets IMG ?= $(IMAGE_TAG_BASE):$(VERSION) FS_IMG ?= quay.io/feastdev/feature-server:$(VERSION) +CJ_IMG ?= quay.io/openshift/origin-cli:4.17 # ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. ENVTEST_K8S_VERSION = 1.30.0 @@ -365,4 +366,4 @@ catalog-push: ## Push a catalog image. .PHONY: related-image-fs related-image-fs: envsubst - FS_IMG=$(FS_IMG) $(ENVSUBST) < config/default/related_image_fs_patch.tmpl > config/default/related_image_fs_patch.yaml + FS_IMG=$(FS_IMG) CJ_IMG=$(CJ_IMG) $(ENVSUBST) < config/default/related_image_fs_patch.tmpl > config/default/related_image_fs_patch.yaml diff --git a/infra/feast-operator/api/v1alpha1/featurestore_types.go b/infra/feast-operator/api/v1alpha1/featurestore_types.go index 1d00163d61b..f6898a2a839 100644 --- a/infra/feast-operator/api/v1alpha1/featurestore_types.go +++ b/infra/feast-operator/api/v1alpha1/featurestore_types.go @@ -18,6 +18,7 @@ package v1alpha1 import ( appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -36,6 +37,7 @@ const ( UIReadyType = "UI" ReadyType = "FeatureStore" AuthorizationReadyType = "Authorization" + CronJobReadyType = "CronJob" // Feast condition reasons: ReadyReason = "Ready" @@ -46,6 +48,7 @@ const ( RegistryFailedReason = "RegistryDeploymentFailed" UIFailedReason = "UIDeploymentFailed" ClientFailedReason = "ClientDeploymentFailed" + CronJobFailedReason = "CronJobDeploymentFailed" KubernetesAuthzFailedReason = "KubernetesAuthorizationDeploymentFailed" // Feast condition messages: @@ -55,6 +58,7 @@ const ( RegistryReadyMessage = "Registry installation complete" UIReadyMessage = "UI installation complete" ClientReadyMessage = "Client installation complete" + CronJobReadyMessage = "CronJob installation complete" KubernetesAuthzReadyMessage = "Kubernetes authorization installation complete" DeploymentNotAvailableMessage = "Deployment is not available" @@ -70,6 +74,7 @@ type FeatureStoreSpec struct { FeastProjectDir *FeastProjectDir `json:"feastProjectDir,omitempty"` Services *FeatureStoreServices `json:"services,omitempty"` AuthzConfig *AuthzConfig `json:"authz,omitempty"` + CronJob *FeastCronJob `json:"cronJob,omitempty"` } // FeastProjectDir defines how to create the feast project directory. @@ -104,6 +109,167 @@ type FeastInitOptions struct { Template string `json:"template,omitempty"` } +// FeastCronJob defines a CronJob to execute against a Feature Store deployment. +type FeastCronJob struct { + // Specification of the desired behavior of a job. + JobSpec *JobSpec `json:"jobSpec,omitempty"` + ContainerConfigs *CronJobContainerConfigs `json:"containerConfigs,omitempty"` + + // The schedule in Cron format, see https://en.wikipedia.org/wiki/Cron. + Schedule string `json:"schedule,omitempty"` + + // The time zone name for the given schedule, see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. + // If not specified, this will default to the time zone of the kube-controller-manager process. + // The set of valid time zone names and the time zone offset is loaded from the system-wide time zone + // database by the API server during CronJob validation and the controller manager during execution. + // If no system-wide time zone database can be found a bundled version of the database is used instead. + // If the time zone name becomes invalid during the lifetime of a CronJob or due to a change in host + // configuration, the controller will stop creating new new Jobs and will create a system event with the + // reason UnknownTimeZone. + // More information can be found in https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/#time-zones + TimeZone *string `json:"timeZone,omitempty"` + + // Optional deadline in seconds for starting the job if it misses scheduled + // time for any reason. Missed jobs executions will be counted as failed ones. + StartingDeadlineSeconds *int64 `json:"startingDeadlineSeconds,omitempty"` + + // Specifies how to treat concurrent executions of a Job. + // Valid values are: + // + // - "Allow" (default): allows CronJobs to run concurrently; + // - "Forbid": forbids concurrent runs, skipping next run if previous run hasn't finished yet; + // - "Replace": cancels currently running job and replaces it with a new one + ConcurrencyPolicy batchv1.ConcurrencyPolicy `json:"concurrencyPolicy,omitempty"` + + // This flag tells the controller to suspend subsequent executions, it does + // not apply to already started executions. + Suspend *bool `json:"suspend,omitempty"` + + // The number of successful finished jobs to retain. Value must be non-negative integer. + SuccessfulJobsHistoryLimit *int32 `json:"successfulJobsHistoryLimit,omitempty"` + + // The number of failed finished jobs to retain. Value must be non-negative integer. + FailedJobsHistoryLimit *int32 `json:"failedJobsHistoryLimit,omitempty"` +} + +// JobSpec describes how the job execution will look like. +type JobSpec struct { + // Specifies the maximum desired number of pods the job should + // run at any given time. The actual number of pods running in steady state will + // be less than this number when ((.spec.completions - .status.successful) < .spec.parallelism), + // i.e. when the work left to do is less than max parallelism. + // More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/ + Parallelism *int32 `json:"parallelism,omitempty"` + + // Specifies the desired number of successfully finished pods the + // job should be run with. Setting to null means that the success of any + // pod signals the success of all pods, and allows parallelism to have any positive + // value. Setting to 1 means that parallelism is limited to 1 and the success of that + // pod signals the success of the job. + // More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/ + Completions *int32 `json:"completions,omitempty"` + + // Specifies the duration in seconds relative to the startTime that the job + // may be continuously active before the system tries to terminate it; value + // must be positive integer. If a Job is suspended (at creation or through an + // update), this timer will effectively be stopped and reset when the Job is + // resumed again. + ActiveDeadlineSeconds *int64 `json:"activeDeadlineSeconds,omitempty"` + + // Specifies the policy of handling failed pods. In particular, it allows to + // specify the set of actions and conditions which need to be + // satisfied to take the associated action. + // If empty, the default behaviour applies - the counter of failed pods, + // represented by the jobs's .status.failed field, is incremented and it is + // checked against the backoffLimit. This field cannot be used in combination + // with restartPolicy=OnFailure. + // + // This field is beta-level. It can be used when the `JobPodFailurePolicy` + // feature gate is enabled (enabled by default). + PodFailurePolicy *batchv1.PodFailurePolicy `json:"podFailurePolicy,omitempty"` + + // Specifies the number of retries before marking this job failed. + BackoffLimit *int32 `json:"backoffLimit,omitempty"` + + // Specifies the limit for the number of retries within an + // index before marking this index as failed. When enabled the number of + // failures per index is kept in the pod's + // batch.kubernetes.io/job-index-failure-count annotation. It can only + // be set when Job's completionMode=Indexed, and the Pod's restart + // policy is Never. The field is immutable. + // This field is beta-level. It can be used when the `JobBackoffLimitPerIndex` + // feature gate is enabled (enabled by default). + BackoffLimitPerIndex *int32 `json:"backoffLimitPerIndex,omitempty"` + + // Specifies the maximal number of failed indexes before marking the Job as + // failed, when backoffLimitPerIndex is set. Once the number of failed + // indexes exceeds this number the entire Job is marked as Failed and its + // execution is terminated. When left as null the job continues execution of + // all of its indexes and is marked with the `Complete` Job condition. + // It can only be specified when backoffLimitPerIndex is set. + // It can be null or up to completions. It is required and must be + // less than or equal to 10^4 when is completions greater than 10^5. + // This field is beta-level. It can be used when the `JobBackoffLimitPerIndex` + // feature gate is enabled (enabled by default). + MaxFailedIndexes *int32 `json:"maxFailedIndexes,omitempty"` + + // ttlSecondsAfterFinished limits the lifetime of a Job that has finished + // execution (either Complete or Failed). If this field is set, + // ttlSecondsAfterFinished after the Job finishes, it is eligible to be + // automatically deleted. When the Job is being deleted, its lifecycle + // guarantees (e.g. finalizers) will be honored. If this field is unset, + // the Job won't be automatically deleted. If this field is set to zero, + // the Job becomes eligible to be deleted immediately after it finishes. + TTLSecondsAfterFinished *int32 `json:"ttlSecondsAfterFinished,omitempty"` + + // completionMode specifies how Pod completions are tracked. It can be + // `NonIndexed` (default) or `Indexed`. + // + // `NonIndexed` means that the Job is considered complete when there have + // been .spec.completions successfully completed Pods. Each Pod completion is + // homologous to each other. + // + // `Indexed` means that the Pods of a + // Job get an associated completion index from 0 to (.spec.completions - 1), + // available in the annotation batch.kubernetes.io/job-completion-index. + // The Job is considered complete when there is one successfully completed Pod + // for each index. + // When value is `Indexed`, .spec.completions must be specified and + // `.spec.parallelism` must be less than or equal to 10^5. + // In addition, The Pod name takes the form + // `$(job-name)-$(index)-$(random-string)`, + // the Pod hostname takes the form `$(job-name)-$(index)`. + // + // More completion modes can be added in the future. + // If the Job controller observes a mode that it doesn't recognize, which + // is possible during upgrades due to version skew, the controller + // skips updates for the Job. + CompletionMode *batchv1.CompletionMode `json:"completionMode,omitempty"` + + // suspend specifies whether the Job controller should create Pods or not. If + // a Job is created with suspend set to true, no Pods are created by the Job + // controller. If a Job is suspended after creation (i.e. the flag goes from + // false to true), the Job controller will delete all active Pods associated + // with this Job. Users must design their workload to gracefully handle this. + // Suspending a Job will reset the StartTime field of the Job, effectively + // resetting the ActiveDeadlineSeconds timer too. + // + Suspend *bool `json:"suspend,omitempty"` + + // podReplacementPolicy specifies when to create replacement Pods. + // Possible values are: + // - TerminatingOrFailed means that we recreate pods + // when they are terminating (has a metadata.deletionTimestamp) or failed. + // - Failed means to wait until a previously created Pod is fully terminated (has phase + // Failed or Succeeded) before creating a replacement Pod. + // + // When using podFailurePolicy, Failed is the the only allowed value. + // TerminatingOrFailed and Failed are allowed values when podFailurePolicy is not in use. + // This is an beta field. To use this, enable the JobPodReplacementPolicy feature toggle. + // This is on by default. + PodReplacementPolicy *batchv1.PodReplacementPolicy `json:"podReplacementPolicy,omitempty"` +} + // FeatureStoreServices defines the desired feast services. An ephemeral onlineStore feature server is deployed by default. type FeatureStoreServices struct { OfflineStore *OfflineStore `json:"offlineStore,omitempty"` @@ -334,6 +500,15 @@ type ServerConfigs struct { VolumeMounts []corev1.VolumeMount `json:"volumeMounts,omitempty"` } +// CronJobContainerConfigs k8s container settings for the CronJob +type CronJobContainerConfigs struct { + ContainerConfigs `json:",inline"` + + // Array of commands to be executed (in order) against a Feature Store deployment. + // Defaults to "feast apply" & "feast materialize-incremental $(date -u +'%Y-%m-%dT%H:%M:%S')" + Commands []string `json:"commands,omitempty"` +} + // ContainerConfigs k8s container settings for the server type ContainerConfigs struct { DefaultCtrConfigs `json:",inline"` @@ -422,7 +597,9 @@ type FeatureStoreStatus struct { // Shows the currently applied feast configuration, including any pertinent defaults Applied FeatureStoreSpec `json:"applied,omitempty"` // ConfigMap in this namespace containing a client `feature_store.yaml` for this feast deployment - ClientConfigMap string `json:"clientConfigMap,omitempty"` + ClientConfigMap string `json:"clientConfigMap,omitempty"` + // CronJob in this namespace for this feast deployment + CronJob string `json:"cronJob,omitempty"` Conditions []metav1.Condition `json:"conditions,omitempty"` FeastVersion string `json:"feastVersion,omitempty"` Phase string `json:"phase,omitempty"` diff --git a/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go b/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go index 87e5b7164af..4fc9b417445 100644 --- a/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/infra/feast-operator/api/v1alpha1/zz_generated.deepcopy.go @@ -22,6 +22,7 @@ package v1alpha1 import ( appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" @@ -69,6 +70,27 @@ func (in *ContainerConfigs) DeepCopy() *ContainerConfigs { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CronJobContainerConfigs) DeepCopyInto(out *CronJobContainerConfigs) { + *out = *in + in.ContainerConfigs.DeepCopyInto(&out.ContainerConfigs) + if in.Commands != nil { + in, out := &in.Commands, &out.Commands + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CronJobContainerConfigs. +func (in *CronJobContainerConfigs) DeepCopy() *CronJobContainerConfigs { + if in == nil { + return nil + } + out := new(CronJobContainerConfigs) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DefaultCtrConfigs) DeepCopyInto(out *DefaultCtrConfigs) { *out = *in @@ -89,6 +111,56 @@ func (in *DefaultCtrConfigs) DeepCopy() *DefaultCtrConfigs { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FeastCronJob) DeepCopyInto(out *FeastCronJob) { + *out = *in + if in.JobSpec != nil { + in, out := &in.JobSpec, &out.JobSpec + *out = new(JobSpec) + (*in).DeepCopyInto(*out) + } + if in.ContainerConfigs != nil { + in, out := &in.ContainerConfigs, &out.ContainerConfigs + *out = new(CronJobContainerConfigs) + (*in).DeepCopyInto(*out) + } + if in.TimeZone != nil { + in, out := &in.TimeZone, &out.TimeZone + *out = new(string) + **out = **in + } + if in.StartingDeadlineSeconds != nil { + in, out := &in.StartingDeadlineSeconds, &out.StartingDeadlineSeconds + *out = new(int64) + **out = **in + } + if in.Suspend != nil { + in, out := &in.Suspend, &out.Suspend + *out = new(bool) + **out = **in + } + if in.SuccessfulJobsHistoryLimit != nil { + in, out := &in.SuccessfulJobsHistoryLimit, &out.SuccessfulJobsHistoryLimit + *out = new(int32) + **out = **in + } + if in.FailedJobsHistoryLimit != nil { + in, out := &in.FailedJobsHistoryLimit, &out.FailedJobsHistoryLimit + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeastCronJob. +func (in *FeastCronJob) DeepCopy() *FeastCronJob { + if in == nil { + return nil + } + out := new(FeastCronJob) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FeastInitOptions) DeepCopyInto(out *FeastInitOptions) { *out = *in @@ -268,6 +340,11 @@ func (in *FeatureStoreSpec) DeepCopyInto(out *FeatureStoreSpec) { *out = new(AuthzConfig) (*in).DeepCopyInto(*out) } + if in.CronJob != nil { + in, out := &in.CronJob, &out.CronJob + *out = new(FeastCronJob) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FeatureStoreSpec. @@ -348,6 +425,76 @@ func (in *GitCloneOptions) DeepCopy() *GitCloneOptions { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JobSpec) DeepCopyInto(out *JobSpec) { + *out = *in + if in.Parallelism != nil { + in, out := &in.Parallelism, &out.Parallelism + *out = new(int32) + **out = **in + } + if in.Completions != nil { + in, out := &in.Completions, &out.Completions + *out = new(int32) + **out = **in + } + if in.ActiveDeadlineSeconds != nil { + in, out := &in.ActiveDeadlineSeconds, &out.ActiveDeadlineSeconds + *out = new(int64) + **out = **in + } + if in.PodFailurePolicy != nil { + in, out := &in.PodFailurePolicy, &out.PodFailurePolicy + *out = new(batchv1.PodFailurePolicy) + (*in).DeepCopyInto(*out) + } + if in.BackoffLimit != nil { + in, out := &in.BackoffLimit, &out.BackoffLimit + *out = new(int32) + **out = **in + } + if in.BackoffLimitPerIndex != nil { + in, out := &in.BackoffLimitPerIndex, &out.BackoffLimitPerIndex + *out = new(int32) + **out = **in + } + if in.MaxFailedIndexes != nil { + in, out := &in.MaxFailedIndexes, &out.MaxFailedIndexes + *out = new(int32) + **out = **in + } + if in.TTLSecondsAfterFinished != nil { + in, out := &in.TTLSecondsAfterFinished, &out.TTLSecondsAfterFinished + *out = new(int32) + **out = **in + } + if in.CompletionMode != nil { + in, out := &in.CompletionMode, &out.CompletionMode + *out = new(batchv1.CompletionMode) + **out = **in + } + if in.Suspend != nil { + in, out := &in.Suspend, &out.Suspend + *out = new(bool) + **out = **in + } + if in.PodReplacementPolicy != nil { + in, out := &in.PodReplacementPolicy, &out.PodReplacementPolicy + *out = new(batchv1.PodReplacementPolicy) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JobSpec. +func (in *JobSpec) DeepCopy() *JobSpec { + if in == nil { + return nil + } + out := new(JobSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *KubernetesAuthz) DeepCopyInto(out *KubernetesAuthz) { *out = *in diff --git a/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml b/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml index 4bb7227f856..0a08b0a62b3 100644 --- a/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml +++ b/infra/feast-operator/config/crd/bases/feast.dev_featurestores.yaml @@ -83,6 +83,384 @@ spec: x-kubernetes-validations: - message: One selection required between kubernetes or oidc. rule: '[has(self.kubernetes), has(self.oidc)].exists_one(c, c)' + cronJob: + description: FeastCronJob defines a CronJob to execute against a Feature + Store deployment. + properties: + concurrencyPolicy: + description: Specifies how to treat concurrent executions of a + Job. + type: string + containerConfigs: + description: CronJobContainerConfigs k8s container settings for + the CronJob + properties: + commands: + description: Array of commands to be executed (in order) against + a Feature Store deployment. + items: + type: string + type: array + env: + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must + be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.labels['''']`, + `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in + the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of + the exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source of a set + of ConfigMaps + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: An optional identifier to prepend to each + key in the ConfigMap. Must be a C_IDENTIFIER. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret must be + defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to + pull a container image + type: string + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount of + compute resources required. + type: object + type: object + type: object + failedJobsHistoryLimit: + description: The number of failed finished jobs to retain. Value + must be non-negative integer. + format: int32 + type: integer + jobSpec: + description: Specification of the desired behavior of a job. + properties: + activeDeadlineSeconds: + description: |- + Specifies the duration in seconds relative to the startTime that the job + may be continuously active before the system tr + format: int64 + type: integer + backoffLimit: + description: Specifies the number of retries before marking + this job failed. + format: int32 + type: integer + backoffLimitPerIndex: + description: |- + Specifies the limit for the number of retries within an + index before marking this index as failed. + format: int32 + type: integer + completionMode: + description: |- + completionMode specifies how Pod completions are tracked. It can be + `NonIndexed` (default) or `Indexed`. + type: string + completions: + description: |- + Specifies the desired number of successfully finished pods the + job should be run with. + format: int32 + type: integer + maxFailedIndexes: + description: |- + Specifies the maximal number of failed indexes before marking the Job as + failed, when backoffLimitPerIndex is set. + format: int32 + type: integer + parallelism: + description: |- + Specifies the maximum desired number of pods the job should + run at any given time. + format: int32 + type: integer + podFailurePolicy: + description: Specifies the policy of handling failed pods. + properties: + rules: + description: A list of pod failure policy rules. The rules + are evaluated in order. + items: + description: PodFailurePolicyRule describes how a pod + failure is handled when the requirements are met. + properties: + action: + description: Specifies the action taken on a pod + failure when the requirements are satisfied. + type: string + onExitCodes: + description: Represents the requirement on the container + exit codes. + properties: + containerName: + description: |- + Restricts the check for exit codes to the container with the + specified name. + type: string + operator: + description: |- + Represents the relationship between the container exit code(s) and the + specified values. + type: string + values: + description: Specifies the set of values. + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + - values + type: object + onPodConditions: + description: |- + Represents the requirement on the pod conditions. The requirement is represented + as a list of pod condition patterns. + items: + description: |- + PodFailurePolicyOnPodConditionsPattern describes a pattern for matching + an actual pod condition type. + properties: + status: + description: Specifies the required Pod condition + status. + type: string + type: + description: Specifies the required Pod condition + type. + type: string + required: + - status + - type + type: object + type: array + x-kubernetes-list-type: atomic + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + required: + - rules + type: object + podReplacementPolicy: + description: podReplacementPolicy specifies when to create + replacement Pods. + type: string + suspend: + description: suspend specifies whether the Job controller + should create Pods or not. + type: boolean + ttlSecondsAfterFinished: + description: |- + ttlSecondsAfterFinished limits the lifetime of a Job that has finished + execution (either Complete or Failed). + format: int32 + type: integer + type: object + schedule: + description: The schedule in Cron format, see https://en.wikipedia.org/wiki/Cron. + type: string + startingDeadlineSeconds: + description: |- + Optional deadline in seconds for starting the job if it misses scheduled + time for any reason. + format: int64 + type: integer + successfulJobsHistoryLimit: + description: The number of successful finished jobs to retain. + Value must be non-negative integer. + format: int32 + type: integer + suspend: + description: |- + This flag tells the controller to suspend subsequent executions, it does + not apply to already started executions. + type: boolean + timeZone: + description: The time zone name for the given schedule, see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. + type: string + type: object feastProject: description: FeastProject is the Feast project id. pattern: ^[A-Za-z0-9][A-Za-z0-9_]*$ @@ -3415,108 +3793,490 @@ spec: secretRef specifies the secret to use for obtaining the StorageOS API credentials. properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + description: |- + volumeName is the human-readable name of the StorageOS volume. Volume + names are only unique within a namespace. + type: string + volumeNamespace: + description: volumeNamespace specifies the scope of + the volume within StorageOS. + type: string + type: object + vsphereVolume: + description: vsphereVolume represents a vSphere volume attached + and mounted on kubelets host machine + properties: + fsType: + description: |- + fsType is filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + storagePolicyID: + description: storagePolicyID is the storage Policy Based + Management (SPBM) profile ID associated with the StoragePolicyName. + type: string + storagePolicyName: + description: storagePolicyName is the storage Policy + Based Management (SPBM) profile name. + type: string + volumePath: + description: volumePath is the path that identifies + vSphere volume vmdk + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + type: object + required: + - feastProject + type: object + status: + description: FeatureStoreStatus defines the observed state of FeatureStore + properties: + applied: + description: Shows the currently applied feast configuration, including + any pertinent defaults + properties: + authz: + description: AuthzConfig defines the authorization settings for + the deployed Feast services. + properties: + kubernetes: + description: |- + KubernetesAuthz provides a way to define the authorization settings using Kubernetes RBAC resources. + https://kubernetes. + properties: + roles: + description: The Kubernetes RBAC roles to be deployed + in the same namespace of the FeatureStore. + items: + type: string + type: array + type: object + oidc: + description: |- + OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. + https://auth0. + properties: + secretRef: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - secretRef + type: object + type: object + x-kubernetes-validations: + - message: One selection required between kubernetes or oidc. + rule: '[has(self.kubernetes), has(self.oidc)].exists_one(c, + c)' + cronJob: + description: FeastCronJob defines a CronJob to execute against + a Feature Store deployment. + properties: + concurrencyPolicy: + description: Specifies how to treat concurrent executions + of a Job. + type: string + containerConfigs: + description: CronJobContainerConfigs k8s container settings + for the CronJob + properties: + commands: + description: Array of commands to be executed (in order) + against a Feature Store deployment. + items: + type: string + type: array + env: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: Name of the environment variable. Must + be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.labels['''']`, + `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source of + a set of ConfigMaps + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: An optional identifier to prepend to + each key in the ConfigMap. Must be a C_IDENTIFIER. type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic type: object - x-kubernetes-map-type: atomic - volumeName: - description: |- - volumeName is the human-readable name of the StorageOS volume. Volume - names are only unique within a namespace. - type: string - volumeNamespace: - description: volumeNamespace specifies the scope of - the volume within StorageOS. - type: string - type: object - vsphereVolume: - description: vsphereVolume represents a vSphere volume attached - and mounted on kubelets host machine - properties: - fsType: - description: |- - fsType is filesystem type to mount. - Must be a filesystem type supported by the host operating system. - Ex. - type: string - storagePolicyID: - description: storagePolicyID is the storage Policy Based - Management (SPBM) profile ID associated with the StoragePolicyName. - type: string - storagePolicyName: - description: storagePolicyName is the storage Policy - Based Management (SPBM) profile name. - type: string - volumePath: - description: volumePath is the path that identifies - vSphere volume vmdk - type: string - required: - - volumePath - type: object - required: - - name - type: object - type: array - type: object - required: - - feastProject - type: object - status: - description: FeatureStoreStatus defines the observed state of FeatureStore - properties: - applied: - description: Shows the currently applied feast configuration, including - any pertinent defaults - properties: - authz: - description: AuthzConfig defines the authorization settings for - the deployed Feast services. - properties: - kubernetes: - description: |- - KubernetesAuthz provides a way to define the authorization settings using Kubernetes RBAC resources. - https://kubernetes. - properties: - roles: - description: The Kubernetes RBAC roles to be deployed - in the same namespace of the FeatureStore. - items: - type: string type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + resources: + description: ResourceRequirements describes the compute + resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount + of compute resources required. + type: object + type: object type: object - oidc: - description: |- - OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. - https://auth0. + failedJobsHistoryLimit: + description: The number of failed finished jobs to retain. + Value must be non-negative integer. + format: int32 + type: integer + jobSpec: + description: Specification of the desired behavior of a job. properties: - secretRef: + activeDeadlineSeconds: description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + Specifies the duration in seconds relative to the startTime that the job + may be continuously active before the system tr + format: int64 + type: integer + backoffLimit: + description: Specifies the number of retries before marking + this job failed. + format: int32 + type: integer + backoffLimitPerIndex: + description: |- + Specifies the limit for the number of retries within an + index before marking this index as failed. + format: int32 + type: integer + completionMode: + description: |- + completionMode specifies how Pod completions are tracked. It can be + `NonIndexed` (default) or `Indexed`. + type: string + completions: + description: |- + Specifies the desired number of successfully finished pods the + job should be run with. + format: int32 + type: integer + maxFailedIndexes: + description: |- + Specifies the maximal number of failed indexes before marking the Job as + failed, when backoffLimitPerIndex is set. + format: int32 + type: integer + parallelism: + description: |- + Specifies the maximum desired number of pods the job should + run at any given time. + format: int32 + type: integer + podFailurePolicy: + description: Specifies the policy of handling failed pods. properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string + rules: + description: A list of pod failure policy rules. The + rules are evaluated in order. + items: + description: PodFailurePolicyRule describes how + a pod failure is handled when the requirements + are met. + properties: + action: + description: Specifies the action taken on a + pod failure when the requirements are satisfied. + type: string + onExitCodes: + description: Represents the requirement on the + container exit codes. + properties: + containerName: + description: |- + Restricts the check for exit codes to the container with the + specified name. + type: string + operator: + description: |- + Represents the relationship between the container exit code(s) and the + specified values. + type: string + values: + description: Specifies the set of values. + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + - values + type: object + onPodConditions: + description: |- + Represents the requirement on the pod conditions. The requirement is represented + as a list of pod condition patterns. + items: + description: |- + PodFailurePolicyOnPodConditionsPattern describes a pattern for matching + an actual pod condition type. + properties: + status: + description: Specifies the required Pod + condition status. + type: string + type: + description: Specifies the required Pod + condition type. + type: string + required: + - status + - type + type: object + type: array + x-kubernetes-list-type: atomic + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + required: + - rules type: object - x-kubernetes-map-type: atomic - required: - - secretRef + podReplacementPolicy: + description: podReplacementPolicy specifies when to create + replacement Pods. + type: string + suspend: + description: suspend specifies whether the Job controller + should create Pods or not. + type: boolean + ttlSecondsAfterFinished: + description: |- + ttlSecondsAfterFinished limits the lifetime of a Job that has finished + execution (either Complete or Failed). + format: int32 + type: integer type: object + schedule: + description: The schedule in Cron format, see https://en.wikipedia.org/wiki/Cron. + type: string + startingDeadlineSeconds: + description: |- + Optional deadline in seconds for starting the job if it misses scheduled + time for any reason. + format: int64 + type: integer + successfulJobsHistoryLimit: + description: The number of successful finished jobs to retain. + Value must be non-negative integer. + format: int32 + type: integer + suspend: + description: |- + This flag tells the controller to suspend subsequent executions, it does + not apply to already started executions. + type: boolean + timeZone: + description: The time zone name for the given schedule, see + https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. + type: string type: object - x-kubernetes-validations: - - message: One selection required between kubernetes or oidc. - rule: '[has(self.kubernetes), has(self.oidc)].exists_one(c, - c)' feastProject: description: FeastProject is the Feast project id. pattern: ^[A-Za-z0-9][A-Za-z0-9_]*$ @@ -7006,6 +7766,9 @@ spec: - type type: object type: array + cronJob: + description: CronJob in this namespace for this feast deployment + type: string feastVersion: type: string phase: diff --git a/infra/feast-operator/config/default/related_image_fs_patch.tmpl b/infra/feast-operator/config/default/related_image_fs_patch.tmpl index f3508836a86..23bf80c98ba 100644 --- a/infra/feast-operator/config/default/related_image_fs_patch.tmpl +++ b/infra/feast-operator/config/default/related_image_fs_patch.tmpl @@ -3,3 +3,8 @@ value: name: RELATED_IMAGE_FEATURE_SERVER value: ${FS_IMG} +- op: replace + path: "/spec/template/spec/containers/0/env/1" + value: + name: RELATED_IMAGE_CRON_JOB + value: ${CJ_IMG} diff --git a/infra/feast-operator/config/default/related_image_fs_patch.yaml b/infra/feast-operator/config/default/related_image_fs_patch.yaml index 8432798da45..22f8c2be40e 100644 --- a/infra/feast-operator/config/default/related_image_fs_patch.yaml +++ b/infra/feast-operator/config/default/related_image_fs_patch.yaml @@ -3,3 +3,8 @@ value: name: RELATED_IMAGE_FEATURE_SERVER value: quay.io/feastdev/feature-server:0.47.0 +- op: replace + path: "/spec/template/spec/containers/0/env/1" + value: + name: RELATED_IMAGE_CRON_JOB + value: quay.io/openshift/origin-cli:4.17 diff --git a/infra/feast-operator/config/manager/manager.yaml b/infra/feast-operator/config/manager/manager.yaml index e7550f0db7f..4c360775c2c 100644 --- a/infra/feast-operator/config/manager/manager.yaml +++ b/infra/feast-operator/config/manager/manager.yaml @@ -73,6 +73,8 @@ spec: env: - name: RELATED_IMAGE_FEATURE_SERVER value: feast:latest + - name: RELATED_IMAGE_CRON_JOB + value: origin-cli:latest livenessProbe: httpGet: path: /healthz diff --git a/infra/feast-operator/config/overlays/odh/kustomization.yaml b/infra/feast-operator/config/overlays/odh/kustomization.yaml index 508757e76fb..cf751d178bd 100644 --- a/infra/feast-operator/config/overlays/odh/kustomization.yaml +++ b/infra/feast-operator/config/overlays/odh/kustomization.yaml @@ -42,3 +42,13 @@ replacements: name: controller-manager fieldPaths: - spec.template.spec.containers.[name=manager].env.[name=RELATED_IMAGE_FEATURE_SERVER].value + - source: + kind: ConfigMap + name: feast-operator-parameters + fieldPath: data.RELATED_IMAGE_CRON_JOB + targets: + - select: + kind: Deployment + name: controller-manager + fieldPaths: + - spec.template.spec.containers.[name=manager].env.[name=RELATED_IMAGE_CRON_JOB].value diff --git a/infra/feast-operator/config/overlays/odh/params.env b/infra/feast-operator/config/overlays/odh/params.env index 48aa6f9a9a9..67cf18a8d72 100644 --- a/infra/feast-operator/config/overlays/odh/params.env +++ b/infra/feast-operator/config/overlays/odh/params.env @@ -1,2 +1,3 @@ RELATED_IMAGE_FEAST_OPERATOR=quay.io/feastdev/feast-operator:0.47.0 RELATED_IMAGE_FEATURE_SERVER=quay.io/feastdev/feature-server:0.47.0 +RELATED_IMAGE_CRON_JOB=quay.io/openshift/origin-cli:4.17 diff --git a/infra/feast-operator/config/rbac/role.yaml b/infra/feast-operator/config/rbac/role.yaml index 7fba75c23a4..207f19ce4a4 100644 --- a/infra/feast-operator/config/rbac/role.yaml +++ b/infra/feast-operator/config/rbac/role.yaml @@ -15,6 +15,18 @@ rules: - list - update - watch +- apiGroups: + - batch + resources: + - cronjobs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - "" resources: @@ -32,10 +44,17 @@ rules: - apiGroups: - "" resources: + - pods - secrets verbs: - get - list +- apiGroups: + - "" + resources: + - pods/exec + verbs: + - create - apiGroups: - feast.dev resources: diff --git a/infra/feast-operator/dist/install.yaml b/infra/feast-operator/dist/install.yaml index 29d8059e403..e5e5d79c85c 100644 --- a/infra/feast-operator/dist/install.yaml +++ b/infra/feast-operator/dist/install.yaml @@ -91,6 +91,384 @@ spec: x-kubernetes-validations: - message: One selection required between kubernetes or oidc. rule: '[has(self.kubernetes), has(self.oidc)].exists_one(c, c)' + cronJob: + description: FeastCronJob defines a CronJob to execute against a Feature + Store deployment. + properties: + concurrencyPolicy: + description: Specifies how to treat concurrent executions of a + Job. + type: string + containerConfigs: + description: CronJobContainerConfigs k8s container settings for + the CronJob + properties: + commands: + description: Array of commands to be executed (in order) against + a Feature Store deployment. + items: + type: string + type: array + env: + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must + be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.labels['''']`, + `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in + the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of + the exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source of a set + of ConfigMaps + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: An optional identifier to prepend to each + key in the ConfigMap. Must be a C_IDENTIFIER. + type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret must be + defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when to + pull a container image + type: string + resources: + description: ResourceRequirements describes the compute resource + requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount of + compute resources required. + type: object + type: object + type: object + failedJobsHistoryLimit: + description: The number of failed finished jobs to retain. Value + must be non-negative integer. + format: int32 + type: integer + jobSpec: + description: Specification of the desired behavior of a job. + properties: + activeDeadlineSeconds: + description: |- + Specifies the duration in seconds relative to the startTime that the job + may be continuously active before the system tr + format: int64 + type: integer + backoffLimit: + description: Specifies the number of retries before marking + this job failed. + format: int32 + type: integer + backoffLimitPerIndex: + description: |- + Specifies the limit for the number of retries within an + index before marking this index as failed. + format: int32 + type: integer + completionMode: + description: |- + completionMode specifies how Pod completions are tracked. It can be + `NonIndexed` (default) or `Indexed`. + type: string + completions: + description: |- + Specifies the desired number of successfully finished pods the + job should be run with. + format: int32 + type: integer + maxFailedIndexes: + description: |- + Specifies the maximal number of failed indexes before marking the Job as + failed, when backoffLimitPerIndex is set. + format: int32 + type: integer + parallelism: + description: |- + Specifies the maximum desired number of pods the job should + run at any given time. + format: int32 + type: integer + podFailurePolicy: + description: Specifies the policy of handling failed pods. + properties: + rules: + description: A list of pod failure policy rules. The rules + are evaluated in order. + items: + description: PodFailurePolicyRule describes how a pod + failure is handled when the requirements are met. + properties: + action: + description: Specifies the action taken on a pod + failure when the requirements are satisfied. + type: string + onExitCodes: + description: Represents the requirement on the container + exit codes. + properties: + containerName: + description: |- + Restricts the check for exit codes to the container with the + specified name. + type: string + operator: + description: |- + Represents the relationship between the container exit code(s) and the + specified values. + type: string + values: + description: Specifies the set of values. + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + - values + type: object + onPodConditions: + description: |- + Represents the requirement on the pod conditions. The requirement is represented + as a list of pod condition patterns. + items: + description: |- + PodFailurePolicyOnPodConditionsPattern describes a pattern for matching + an actual pod condition type. + properties: + status: + description: Specifies the required Pod condition + status. + type: string + type: + description: Specifies the required Pod condition + type. + type: string + required: + - status + - type + type: object + type: array + x-kubernetes-list-type: atomic + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + required: + - rules + type: object + podReplacementPolicy: + description: podReplacementPolicy specifies when to create + replacement Pods. + type: string + suspend: + description: suspend specifies whether the Job controller + should create Pods or not. + type: boolean + ttlSecondsAfterFinished: + description: |- + ttlSecondsAfterFinished limits the lifetime of a Job that has finished + execution (either Complete or Failed). + format: int32 + type: integer + type: object + schedule: + description: The schedule in Cron format, see https://en.wikipedia.org/wiki/Cron. + type: string + startingDeadlineSeconds: + description: |- + Optional deadline in seconds for starting the job if it misses scheduled + time for any reason. + format: int64 + type: integer + successfulJobsHistoryLimit: + description: The number of successful finished jobs to retain. + Value must be non-negative integer. + format: int32 + type: integer + suspend: + description: |- + This flag tells the controller to suspend subsequent executions, it does + not apply to already started executions. + type: boolean + timeZone: + description: The time zone name for the given schedule, see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. + type: string + type: object feastProject: description: FeastProject is the Feast project id. pattern: ^[A-Za-z0-9][A-Za-z0-9_]*$ @@ -3423,108 +3801,490 @@ spec: secretRef specifies the secret to use for obtaining the StorageOS API credentials. properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + volumeName: + description: |- + volumeName is the human-readable name of the StorageOS volume. Volume + names are only unique within a namespace. + type: string + volumeNamespace: + description: volumeNamespace specifies the scope of + the volume within StorageOS. + type: string + type: object + vsphereVolume: + description: vsphereVolume represents a vSphere volume attached + and mounted on kubelets host machine + properties: + fsType: + description: |- + fsType is filesystem type to mount. + Must be a filesystem type supported by the host operating system. + Ex. + type: string + storagePolicyID: + description: storagePolicyID is the storage Policy Based + Management (SPBM) profile ID associated with the StoragePolicyName. + type: string + storagePolicyName: + description: storagePolicyName is the storage Policy + Based Management (SPBM) profile name. + type: string + volumePath: + description: volumePath is the path that identifies + vSphere volume vmdk + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + type: object + required: + - feastProject + type: object + status: + description: FeatureStoreStatus defines the observed state of FeatureStore + properties: + applied: + description: Shows the currently applied feast configuration, including + any pertinent defaults + properties: + authz: + description: AuthzConfig defines the authorization settings for + the deployed Feast services. + properties: + kubernetes: + description: |- + KubernetesAuthz provides a way to define the authorization settings using Kubernetes RBAC resources. + https://kubernetes. + properties: + roles: + description: The Kubernetes RBAC roles to be deployed + in the same namespace of the FeatureStore. + items: + type: string + type: array + type: object + oidc: + description: |- + OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. + https://auth0. + properties: + secretRef: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + type: object + x-kubernetes-map-type: atomic + required: + - secretRef + type: object + type: object + x-kubernetes-validations: + - message: One selection required between kubernetes or oidc. + rule: '[has(self.kubernetes), has(self.oidc)].exists_one(c, + c)' + cronJob: + description: FeastCronJob defines a CronJob to execute against + a Feature Store deployment. + properties: + concurrencyPolicy: + description: Specifies how to treat concurrent executions + of a Job. + type: string + containerConfigs: + description: CronJobContainerConfigs k8s container settings + for the CronJob + properties: + commands: + description: Array of commands to be executed (in order) + against a Feature Store deployment. + items: + type: string + type: array + env: + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: Name of the environment variable. Must + be a C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: 'Selects a field of the pod: supports + metadata.name, metadata.namespace, `metadata.labels['''']`, + `metadata.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits. + properties: + containerName: + description: 'Container name: required for + volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the + pod's namespace + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + envFrom: + items: + description: EnvFromSource represents the source of + a set of ConfigMaps + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the ConfigMap must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: An optional identifier to prepend to + each key in the ConfigMap. Must be a C_IDENTIFIER. type: string + secretRef: + description: The Secret to select from + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. + type: string + optional: + description: Specify whether the Secret must + be defined + type: boolean + type: object + x-kubernetes-map-type: atomic type: object - x-kubernetes-map-type: atomic - volumeName: - description: |- - volumeName is the human-readable name of the StorageOS volume. Volume - names are only unique within a namespace. - type: string - volumeNamespace: - description: volumeNamespace specifies the scope of - the volume within StorageOS. - type: string - type: object - vsphereVolume: - description: vsphereVolume represents a vSphere volume attached - and mounted on kubelets host machine - properties: - fsType: - description: |- - fsType is filesystem type to mount. - Must be a filesystem type supported by the host operating system. - Ex. - type: string - storagePolicyID: - description: storagePolicyID is the storage Policy Based - Management (SPBM) profile ID associated with the StoragePolicyName. - type: string - storagePolicyName: - description: storagePolicyName is the storage Policy - Based Management (SPBM) profile name. - type: string - volumePath: - description: volumePath is the path that identifies - vSphere volume vmdk - type: string - required: - - volumePath - type: object - required: - - name - type: object - type: array - type: object - required: - - feastProject - type: object - status: - description: FeatureStoreStatus defines the observed state of FeatureStore - properties: - applied: - description: Shows the currently applied feast configuration, including - any pertinent defaults - properties: - authz: - description: AuthzConfig defines the authorization settings for - the deployed Feast services. - properties: - kubernetes: - description: |- - KubernetesAuthz provides a way to define the authorization settings using Kubernetes RBAC resources. - https://kubernetes. - properties: - roles: - description: The Kubernetes RBAC roles to be deployed - in the same namespace of the FeatureStore. - items: - type: string type: array + image: + type: string + imagePullPolicy: + description: PullPolicy describes a policy for if/when + to pull a container image + type: string + resources: + description: ResourceRequirements describes the compute + resource requirements. + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes. + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Requests describes the minimum amount + of compute resources required. + type: object + type: object type: object - oidc: - description: |- - OidcAuthz defines the authorization settings for deployments using an Open ID Connect identity provider. - https://auth0. + failedJobsHistoryLimit: + description: The number of failed finished jobs to retain. + Value must be non-negative integer. + format: int32 + type: integer + jobSpec: + description: Specification of the desired behavior of a job. properties: - secretRef: + activeDeadlineSeconds: description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. + Specifies the duration in seconds relative to the startTime that the job + may be continuously active before the system tr + format: int64 + type: integer + backoffLimit: + description: Specifies the number of retries before marking + this job failed. + format: int32 + type: integer + backoffLimitPerIndex: + description: |- + Specifies the limit for the number of retries within an + index before marking this index as failed. + format: int32 + type: integer + completionMode: + description: |- + completionMode specifies how Pod completions are tracked. It can be + `NonIndexed` (default) or `Indexed`. + type: string + completions: + description: |- + Specifies the desired number of successfully finished pods the + job should be run with. + format: int32 + type: integer + maxFailedIndexes: + description: |- + Specifies the maximal number of failed indexes before marking the Job as + failed, when backoffLimitPerIndex is set. + format: int32 + type: integer + parallelism: + description: |- + Specifies the maximum desired number of pods the job should + run at any given time. + format: int32 + type: integer + podFailurePolicy: + description: Specifies the policy of handling failed pods. properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. - type: string + rules: + description: A list of pod failure policy rules. The + rules are evaluated in order. + items: + description: PodFailurePolicyRule describes how + a pod failure is handled when the requirements + are met. + properties: + action: + description: Specifies the action taken on a + pod failure when the requirements are satisfied. + type: string + onExitCodes: + description: Represents the requirement on the + container exit codes. + properties: + containerName: + description: |- + Restricts the check for exit codes to the container with the + specified name. + type: string + operator: + description: |- + Represents the relationship between the container exit code(s) and the + specified values. + type: string + values: + description: Specifies the set of values. + items: + format: int32 + type: integer + type: array + x-kubernetes-list-type: set + required: + - operator + - values + type: object + onPodConditions: + description: |- + Represents the requirement on the pod conditions. The requirement is represented + as a list of pod condition patterns. + items: + description: |- + PodFailurePolicyOnPodConditionsPattern describes a pattern for matching + an actual pod condition type. + properties: + status: + description: Specifies the required Pod + condition status. + type: string + type: + description: Specifies the required Pod + condition type. + type: string + required: + - status + - type + type: object + type: array + x-kubernetes-list-type: atomic + required: + - action + type: object + type: array + x-kubernetes-list-type: atomic + required: + - rules type: object - x-kubernetes-map-type: atomic - required: - - secretRef + podReplacementPolicy: + description: podReplacementPolicy specifies when to create + replacement Pods. + type: string + suspend: + description: suspend specifies whether the Job controller + should create Pods or not. + type: boolean + ttlSecondsAfterFinished: + description: |- + ttlSecondsAfterFinished limits the lifetime of a Job that has finished + execution (either Complete or Failed). + format: int32 + type: integer type: object + schedule: + description: The schedule in Cron format, see https://en.wikipedia.org/wiki/Cron. + type: string + startingDeadlineSeconds: + description: |- + Optional deadline in seconds for starting the job if it misses scheduled + time for any reason. + format: int64 + type: integer + successfulJobsHistoryLimit: + description: The number of successful finished jobs to retain. + Value must be non-negative integer. + format: int32 + type: integer + suspend: + description: |- + This flag tells the controller to suspend subsequent executions, it does + not apply to already started executions. + type: boolean + timeZone: + description: The time zone name for the given schedule, see + https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. + type: string type: object - x-kubernetes-validations: - - message: One selection required between kubernetes or oidc. - rule: '[has(self.kubernetes), has(self.oidc)].exists_one(c, - c)' feastProject: description: FeastProject is the Feast project id. pattern: ^[A-Za-z0-9][A-Za-z0-9_]*$ @@ -7014,6 +7774,9 @@ spec: - type type: object type: array + cronJob: + description: CronJob in this namespace for this feast deployment + type: string feastVersion: type: string phase: @@ -7154,6 +7917,18 @@ rules: - list - update - watch +- apiGroups: + - batch + resources: + - cronjobs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - "" resources: @@ -7171,10 +7946,17 @@ rules: - apiGroups: - "" resources: + - pods - secrets verbs: - get - list +- apiGroups: + - "" + resources: + - pods/exec + verbs: + - create - apiGroups: - feast.dev resources: @@ -7357,6 +8139,8 @@ spec: env: - name: RELATED_IMAGE_FEATURE_SERVER value: quay.io/feastdev/feature-server:0.47.0 + - name: RELATED_IMAGE_CRON_JOB + value: quay.io/openshift/origin-cli:4.17 image: quay.io/feastdev/feast-operator:0.47.0 livenessProbe: httpGet: diff --git a/infra/feast-operator/docs/api/markdown/ref.md b/infra/feast-operator/docs/api/markdown/ref.md index aefb5abca76..0082ede4000 100644 --- a/infra/feast-operator/docs/api/markdown/ref.md +++ b/infra/feast-operator/docs/api/markdown/ref.md @@ -35,6 +35,7 @@ _Appears in:_ ContainerConfigs k8s container settings for the server _Appears in:_ +- [CronJobContainerConfigs](#cronjobcontainerconfigs) - [ServerConfigs](#serverconfigs) | Field | Description | @@ -46,6 +47,26 @@ _Appears in:_ | `resources` _[ResourceRequirements](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#resourcerequirements-v1-core)_ | | +#### CronJobContainerConfigs + + + +CronJobContainerConfigs k8s container settings for the CronJob + +_Appears in:_ +- [FeastCronJob](#feastcronjob) + +| Field | Description | +| --- | --- | +| `image` _string_ | | +| `env` _[EnvVar](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#envvar-v1-core)_ | | +| `envFrom` _[EnvFromSource](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#envfromsource-v1-core)_ | | +| `imagePullPolicy` _[PullPolicy](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#pullpolicy-v1-core)_ | | +| `resources` _[ResourceRequirements](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#resourcerequirements-v1-core)_ | | +| `commands` _string array_ | Array of commands to be executed (in order) against a Feature Store deployment. +Defaults to "feast apply" & "feast materialize-incremental $(date -u +'%Y-%m-%dT%H:%M:%S')" | + + #### DefaultCtrConfigs @@ -54,6 +75,7 @@ DefaultCtrConfigs k8s container settings that are applied by default _Appears in:_ - [ContainerConfigs](#containerconfigs) +- [CronJobContainerConfigs](#cronjobcontainerconfigs) - [ServerConfigs](#serverconfigs) | Field | Description | @@ -61,6 +83,44 @@ _Appears in:_ | `image` _string_ | | +#### FeastCronJob + + + +FeastCronJob defines a CronJob to execute against a Feature Store deployment. + +_Appears in:_ +- [FeatureStoreSpec](#featurestorespec) + +| Field | Description | +| --- | --- | +| `jobSpec` _[JobSpec](#jobspec)_ | Specification of the desired behavior of a job. | +| `containerConfigs` _[CronJobContainerConfigs](#cronjobcontainerconfigs)_ | | +| `schedule` _string_ | The schedule in Cron format, see https://en.wikipedia.org/wiki/Cron. | +| `timeZone` _string_ | The time zone name for the given schedule, see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones. +If not specified, this will default to the time zone of the kube-controller-manager process. +The set of valid time zone names and the time zone offset is loaded from the system-wide time zone +database by the API server during CronJob validation and the controller manager during execution. +If no system-wide time zone database can be found a bundled version of the database is used instead. +If the time zone name becomes invalid during the lifetime of a CronJob or due to a change in host +configuration, the controller will stop creating new new Jobs and will create a system event with the +reason UnknownTimeZone. +More information can be found in https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/#time-zones | +| `startingDeadlineSeconds` _integer_ | Optional deadline in seconds for starting the job if it misses scheduled +time for any reason. Missed jobs executions will be counted as failed ones. | +| `concurrencyPolicy` _[ConcurrencyPolicy](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#concurrencypolicy-v1-batch)_ | Specifies how to treat concurrent executions of a Job. +Valid values are: + + +- "Allow" (default): allows CronJobs to run concurrently; +- "Forbid": forbids concurrent runs, skipping next run if previous run hasn't finished yet; +- "Replace": cancels currently running job and replaces it with a new one | +| `suspend` _boolean_ | This flag tells the controller to suspend subsequent executions, it does +not apply to already started executions. | +| `successfulJobsHistoryLimit` _integer_ | The number of successful finished jobs to retain. Value must be non-negative integer. | +| `failedJobsHistoryLimit` _integer_ | The number of failed finished jobs to retain. Value must be non-negative integer. | + + #### FeastInitOptions @@ -159,6 +219,7 @@ _Appears in:_ | `feastProjectDir` _[FeastProjectDir](#feastprojectdir)_ | | | `services` _[FeatureStoreServices](#featurestoreservices)_ | | | `authz` _[AuthzConfig](#authzconfig)_ | | +| `cronJob` _[FeastCronJob](#feastcronjob)_ | | #### FeatureStoreStatus @@ -174,6 +235,7 @@ _Appears in:_ | --- | --- | | `applied` _[FeatureStoreSpec](#featurestorespec)_ | Shows the currently applied feast configuration, including any pertinent defaults | | `clientConfigMap` _string_ | ConfigMap in this namespace containing a client `feature_store.yaml` for this feast deployment | +| `cronJob` _string_ | CronJob in this namespace for this feast deployment | | `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#condition-v1-meta) array_ | | | `feastVersion` _string_ | | | `phase` _string_ | | @@ -201,6 +263,116 @@ OR 'url."https://api:\${TOKEN}@github.com/".insteadOf': 'https://github.com/' | | `envFrom` _[EnvFromSource](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#envfromsource-v1-core)_ | | +#### JobSpec + + + +JobSpec describes how the job execution will look like. + +_Appears in:_ +- [FeastCronJob](#feastcronjob) + +| Field | Description | +| --- | --- | +| `parallelism` _integer_ | Specifies the maximum desired number of pods the job should +run at any given time. The actual number of pods running in steady state will +be less than this number when ((.spec.completions - .status.successful) < .spec.parallelism), +i.e. when the work left to do is less than max parallelism. +More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/ | +| `completions` _integer_ | Specifies the desired number of successfully finished pods the +job should be run with. Setting to null means that the success of any +pod signals the success of all pods, and allows parallelism to have any positive +value. Setting to 1 means that parallelism is limited to 1 and the success of that +pod signals the success of the job. +More info: https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/ | +| `activeDeadlineSeconds` _integer_ | Specifies the duration in seconds relative to the startTime that the job +may be continuously active before the system tries to terminate it; value +must be positive integer. If a Job is suspended (at creation or through an +update), this timer will effectively be stopped and reset when the Job is +resumed again. | +| `podFailurePolicy` _[PodFailurePolicy](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#podfailurepolicy-v1-batch)_ | Specifies the policy of handling failed pods. In particular, it allows to +specify the set of actions and conditions which need to be +satisfied to take the associated action. +If empty, the default behaviour applies - the counter of failed pods, +represented by the jobs's .status.failed field, is incremented and it is +checked against the backoffLimit. This field cannot be used in combination +with restartPolicy=OnFailure. + + +This field is beta-level. It can be used when the `JobPodFailurePolicy` +feature gate is enabled (enabled by default). | +| `backoffLimit` _integer_ | Specifies the number of retries before marking this job failed. | +| `backoffLimitPerIndex` _integer_ | Specifies the limit for the number of retries within an +index before marking this index as failed. When enabled the number of +failures per index is kept in the pod's +batch.kubernetes.io/job-index-failure-count annotation. It can only +be set when Job's completionMode=Indexed, and the Pod's restart +policy is Never. The field is immutable. +This field is beta-level. It can be used when the `JobBackoffLimitPerIndex` +feature gate is enabled (enabled by default). | +| `maxFailedIndexes` _integer_ | Specifies the maximal number of failed indexes before marking the Job as +failed, when backoffLimitPerIndex is set. Once the number of failed +indexes exceeds this number the entire Job is marked as Failed and its +execution is terminated. When left as null the job continues execution of +all of its indexes and is marked with the `Complete` Job condition. +It can only be specified when backoffLimitPerIndex is set. +It can be null or up to completions. It is required and must be +less than or equal to 10^4 when is completions greater than 10^5. +This field is beta-level. It can be used when the `JobBackoffLimitPerIndex` +feature gate is enabled (enabled by default). | +| `ttlSecondsAfterFinished` _integer_ | ttlSecondsAfterFinished limits the lifetime of a Job that has finished +execution (either Complete or Failed). If this field is set, +ttlSecondsAfterFinished after the Job finishes, it is eligible to be +automatically deleted. When the Job is being deleted, its lifecycle +guarantees (e.g. finalizers) will be honored. If this field is unset, +the Job won't be automatically deleted. If this field is set to zero, +the Job becomes eligible to be deleted immediately after it finishes. | +| `completionMode` _[CompletionMode](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#completionmode-v1-batch)_ | completionMode specifies how Pod completions are tracked. It can be +`NonIndexed` (default) or `Indexed`. + + +`NonIndexed` means that the Job is considered complete when there have +been .spec.completions successfully completed Pods. Each Pod completion is +homologous to each other. + + +`Indexed` means that the Pods of a +Job get an associated completion index from 0 to (.spec.completions - 1), +available in the annotation batch.kubernetes.io/job-completion-index. +The Job is considered complete when there is one successfully completed Pod +for each index. +When value is `Indexed`, .spec.completions must be specified and +`.spec.parallelism` must be less than or equal to 10^5. +In addition, The Pod name takes the form +`$(job-name)-$(index)-$(random-string)`, +the Pod hostname takes the form `$(job-name)-$(index)`. + + +More completion modes can be added in the future. +If the Job controller observes a mode that it doesn't recognize, which +is possible during upgrades due to version skew, the controller +skips updates for the Job. | +| `suspend` _boolean_ | suspend specifies whether the Job controller should create Pods or not. If +a Job is created with suspend set to true, no Pods are created by the Job +controller. If a Job is suspended after creation (i.e. the flag goes from +false to true), the Job controller will delete all active Pods associated +with this Job. Users must design their workload to gracefully handle this. +Suspending a Job will reset the StartTime field of the Job, effectively +resetting the ActiveDeadlineSeconds timer too. | +| `podReplacementPolicy` _[PodReplacementPolicy](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#podreplacementpolicy-v1-batch)_ | podReplacementPolicy specifies when to create replacement Pods. +Possible values are: +- TerminatingOrFailed means that we recreate pods + when they are terminating (has a metadata.deletionTimestamp) or failed. +- Failed means to wait until a previously created Pod is fully terminated (has phase + Failed or Succeeded) before creating a replacement Pod. + + +When using podFailurePolicy, Failed is the the only allowed value. +TerminatingOrFailed and Failed are allowed values when podFailurePolicy is not in use. +This is an beta field. To use this, enable the JobPodReplacementPolicy feature toggle. +This is on by default. | + + #### KubernetesAuthz @@ -381,6 +553,7 @@ OptionalCtrConfigs k8s container settings that are optional _Appears in:_ - [ContainerConfigs](#containerconfigs) +- [CronJobContainerConfigs](#cronjobcontainerconfigs) - [ServerConfigs](#serverconfigs) | Field | Description | diff --git a/infra/feast-operator/internal/controller/authz/authz.go b/infra/feast-operator/internal/controller/authz/authz.go index 122e9f69457..b0f8aaafe49 100644 --- a/infra/feast-operator/internal/controller/authz/authz.go +++ b/infra/feast-operator/internal/controller/authz/authz.go @@ -179,7 +179,8 @@ func (authz *FeastAuthorization) setAuthRole(role *rbacv1.Role) error { func (authz *FeastAuthorization) getLabels() map[string]string { return map[string]string{ - services.NameLabelKey: authz.Handler.FeatureStore.Name, + services.NameLabelKey: authz.Handler.FeatureStore.Name, + services.ServiceTypeLabelKey: string(services.AuthzFeastType), } } diff --git a/infra/feast-operator/internal/controller/featurestore_controller.go b/infra/feast-operator/internal/controller/featurestore_controller.go index c3353c859f8..e9cbb219acb 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller.go +++ b/infra/feast-operator/internal/controller/featurestore_controller.go @@ -22,6 +22,7 @@ import ( "time" appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -59,8 +60,10 @@ type FeatureStoreReconciler struct { // +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;create;update;watch;delete // +kubebuilder:rbac:groups=core,resources=services;configmaps;persistentvolumeclaims;serviceaccounts,verbs=get;list;create;update;watch;delete // +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles;rolebindings,verbs=get;list;create;update;watch;delete -// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list +// +kubebuilder:rbac:groups=core,resources=secrets;pods,verbs=get;list +// +kubebuilder:rbac:groups=core,resources=pods/exec,verbs=create // +kubebuilder:rbac:groups=route.openshift.io,resources=routes,verbs=get;list;create;update;watch;delete +// +kubebuilder:rbac:groups=batch,resources=cronjobs,verbs=get;list;watch;create;update;patch;delete // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. @@ -188,6 +191,7 @@ func (r *FeatureStoreReconciler) SetupWithManager(mgr ctrl.Manager) error { Owns(&corev1.ServiceAccount{}). Owns(&rbacv1.RoleBinding{}). Owns(&rbacv1.Role{}). + Owns(&batchv1.CronJob{}). Watches(&feastdevv1alpha1.FeatureStore{}, handler.EnqueueRequestsFromMapFunc(r.mapFeastRefsToFeastRequests)) if services.IsOpenShift() { bldr = bldr.Owns(&routev1.Route{}) diff --git a/infra/feast-operator/internal/controller/featurestore_controller_cronjob_test.go b/infra/feast-operator/internal/controller/featurestore_controller_cronjob_test.go new file mode 100644 index 00000000000..b72e191ae34 --- /dev/null +++ b/infra/feast-operator/internal/controller/featurestore_controller_cronjob_test.go @@ -0,0 +1,327 @@ +/* +Copyright 2024 Feast Community. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package controller + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" + "github.com/feast-dev/feast/infra/feast-operator/internal/controller/services" +) + +var _ = Describe("FeatureStore Controller - Feast CronJob", func() { + Context("When reconciling a FeatureStore resource", func() { + const resourceName = "test-cronjob" + + ctx := context.Background() + + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + Namespace: "default", + } + featurestore := &feastdevv1alpha1.FeatureStore{} + + BeforeEach(func() { + By("creating the custom resource for the Kind FeatureStore") + err := k8sClient.Get(ctx, typeNamespacedName, featurestore) + if err != nil && errors.IsNotFound(err) { + resource := &feastdevv1alpha1.FeatureStore{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: "default", + }, + Spec: feastdevv1alpha1.FeatureStoreSpec{FeastProject: feastProject}, + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + }) + AfterEach(func() { + resource := &feastdevv1alpha1.FeatureStore{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance FeatureStore") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + }) + + It("should successfully reconcile the resource with a CronJob", func() { + By("Reconciling the created resource with default CronJob settings") + controllerReconciler := &FeatureStoreReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + + resource := &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + feast := services.FeastServices{ + Handler: handler.FeastHandler{ + Client: controllerReconciler.Client, + Context: ctx, + Scheme: controllerReconciler.Scheme, + FeatureStore: resource, + }, + } + + objMeta := feast.GetObjectMeta() + Expect(resource.Status).NotTo(BeNil()) + Expect(resource.Status.CronJob).To(Equal(objMeta.Name)) + Expect(resource.Status.Applied.CronJob.Schedule).NotTo(BeEmpty()) + + Expect(resource.Status.Conditions).NotTo(BeEmpty()) + cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.CronJobReadyType) + Expect(cond).ToNot(BeNil()) + Expect(cond.Type).To(Equal(feastdevv1alpha1.CronJobReadyType)) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) + Expect(cond.Message).To(Equal(feastdevv1alpha1.CronJobReadyMessage)) + + // check CronJob + cronJob := &batchv1.CronJob{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: objMeta.Name, + Namespace: objMeta.Namespace, + }, cronJob) + Expect(err).NotTo(HaveOccurred()) + Expect(controllerutil.HasControllerReference(cronJob)).To(BeTrue()) + Expect(cronJob.Spec.Schedule).To(Equal("@yearly")) + suspend := true + Expect(cronJob.Spec.Suspend).To(Equal(&suspend)) + Expect(cronJob.Spec.ConcurrencyPolicy).To(Equal(batchv1.ReplaceConcurrent)) + startingDeadlineSeconds := int64(5) + Expect(cronJob.Spec.StartingDeadlineSeconds).To(Equal(&startingDeadlineSeconds)) + + checkCronJob(resource.Status.Applied.CronJob, cronJob.Spec) + }) + + It("should successfully reconcile the resource with a CronJob", func() { + By("Reconciling the created resource with custom CronJob configurations") + controllerReconciler := &FeatureStoreReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + + resource := &feastdevv1alpha1.FeatureStore{} + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + schedule := "5 4 * * *" + image := "image:latest" + timeZone := "Etc/UTC" + suspend := false + int32Var := int32(2) + int64Var := int64(10) + completionMode := batchv1.IndexedCompletion + replacementPolicy := batchv1.TerminatingOrFailed + always := corev1.PullAlways + startingDeadlineSeconds := int64(6) + resource.Spec.CronJob = &feastdevv1alpha1.FeastCronJob{ + Schedule: schedule, + StartingDeadlineSeconds: &startingDeadlineSeconds, + TimeZone: &timeZone, + ConcurrencyPolicy: batchv1.ForbidConcurrent, + Suspend: &suspend, + SuccessfulJobsHistoryLimit: &int32Var, + FailedJobsHistoryLimit: &int32Var, + ContainerConfigs: &feastdevv1alpha1.CronJobContainerConfigs{ + Commands: []string{ + "feast apply", + "feast feature-views list", + "feast entities list", + "feast on-demand-feature-views list", + "feast projects list", + }, + ContainerConfigs: feastdevv1alpha1.ContainerConfigs{ + DefaultCtrConfigs: feastdevv1alpha1.DefaultCtrConfigs{ + Image: &image, + }, + OptionalCtrConfigs: feastdevv1alpha1.OptionalCtrConfigs{ + Env: &[]corev1.EnvVar{ + { + Name: "test", + Value: "var", + }, + }, + ImagePullPolicy: &always, + }, + }, + }, + JobSpec: &feastdevv1alpha1.JobSpec{ + Parallelism: &int32Var, + Completions: &int32Var, + ActiveDeadlineSeconds: &int64Var, + BackoffLimit: &int32Var, + BackoffLimitPerIndex: &int32Var, + MaxFailedIndexes: &int32Var, + TTLSecondsAfterFinished: &int32Var, + CompletionMode: &completionMode, + Suspend: &suspend, + PodReplacementPolicy: &replacementPolicy, + }, + } + err = k8sClient.Update(ctx, resource) + Expect(err).NotTo(HaveOccurred()) + + _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + Expect(err).NotTo(HaveOccurred()) + err = k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + feast := services.FeastServices{ + Handler: handler.FeastHandler{ + Client: controllerReconciler.Client, + Context: ctx, + Scheme: controllerReconciler.Scheme, + FeatureStore: resource, + }, + } + + objMeta := feast.GetObjectMeta() + Expect(resource.Status).NotTo(BeNil()) + Expect(resource.Status.CronJob).To(Equal(objMeta.Name)) + Expect(resource.Status.Applied.CronJob.Schedule).NotTo(BeEmpty()) + Expect(resource.Status.Applied.CronJob.Suspend).To(Equal(&suspend)) + Expect(resource.Status.Applied.CronJob.ConcurrencyPolicy).To(Equal(batchv1.ForbidConcurrent)) + Expect(resource.Status.Applied.CronJob.FailedJobsHistoryLimit).To(Equal(&int32Var)) + + Expect(resource.Status.Conditions).NotTo(BeEmpty()) + cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.CronJobReadyType) + Expect(cond).ToNot(BeNil()) + Expect(cond.Type).To(Equal(feastdevv1alpha1.CronJobReadyType)) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) + Expect(cond.Message).To(Equal(feastdevv1alpha1.CronJobReadyMessage)) + + // check CronJob + cronJob := &batchv1.CronJob{} + err = k8sClient.Get(ctx, types.NamespacedName{ + Name: objMeta.Name, + Namespace: objMeta.Namespace, + }, cronJob) + Expect(err).NotTo(HaveOccurred()) + + Expect(cronJob.Spec.Schedule).To(Equal(schedule)) + Expect(cronJob.Spec.StartingDeadlineSeconds).To(Equal(&startingDeadlineSeconds)) + Expect(cronJob.Spec.JobTemplate.Spec.Parallelism).To(Equal(&int32Var)) + + checkCronJob(resource.Status.Applied.CronJob, cronJob.Spec) + }) + }) +}) + +func checkCronJob(appliedFeastCronJob *feastdevv1alpha1.FeastCronJob, cronSpec batchv1.CronJobSpec) { + Expect(appliedFeastCronJob).NotTo(BeNil()) + Expect(appliedFeastCronJob.Schedule).To(Equal(cronSpec.Schedule)) + if len(appliedFeastCronJob.ConcurrencyPolicy) > 0 { + Expect(appliedFeastCronJob.ConcurrencyPolicy).To(Equal(cronSpec.ConcurrencyPolicy)) + } + if appliedFeastCronJob.FailedJobsHistoryLimit != nil { + Expect(appliedFeastCronJob.FailedJobsHistoryLimit).To(Equal(cronSpec.FailedJobsHistoryLimit)) + } + if appliedFeastCronJob.StartingDeadlineSeconds != nil { + Expect(appliedFeastCronJob.StartingDeadlineSeconds).To(Equal(cronSpec.StartingDeadlineSeconds)) + } + if appliedFeastCronJob.SuccessfulJobsHistoryLimit != nil { + Expect(appliedFeastCronJob.SuccessfulJobsHistoryLimit).To(Equal(cronSpec.SuccessfulJobsHistoryLimit)) + } + if appliedFeastCronJob.Suspend != nil { + Expect(appliedFeastCronJob.Suspend).To(Equal(cronSpec.Suspend)) + } + if appliedFeastCronJob.TimeZone != nil { + Expect(appliedFeastCronJob.TimeZone).To(Equal(cronSpec.TimeZone)) + } + checkJobSpec(appliedFeastCronJob.JobSpec, cronSpec.JobTemplate.Spec) + checkCronJobContainers(appliedFeastCronJob.ContainerConfigs, cronSpec.JobTemplate.Spec.Template.Spec) +} + +func checkJobSpec(appliedFeastJobSpec *feastdevv1alpha1.JobSpec, jobSpec batchv1.JobSpec) { + if appliedFeastJobSpec != nil { + if appliedFeastJobSpec.ActiveDeadlineSeconds != nil { + Expect(appliedFeastJobSpec.ActiveDeadlineSeconds).To(Equal(jobSpec.ActiveDeadlineSeconds)) + } + if appliedFeastJobSpec.BackoffLimit != nil { + Expect(appliedFeastJobSpec.BackoffLimit).To(Equal(jobSpec.BackoffLimit)) + } + if appliedFeastJobSpec.BackoffLimitPerIndex != nil { + Expect(appliedFeastJobSpec.BackoffLimitPerIndex).To(Equal(jobSpec.BackoffLimitPerIndex)) + } + if appliedFeastJobSpec.CompletionMode != nil { + Expect(appliedFeastJobSpec.CompletionMode).To(Equal(jobSpec.CompletionMode)) + } + if appliedFeastJobSpec.Completions != nil { + Expect(appliedFeastJobSpec.Completions).To(Equal(jobSpec.Completions)) + } + if appliedFeastJobSpec.MaxFailedIndexes != nil { + Expect(appliedFeastJobSpec.MaxFailedIndexes).To(Equal(jobSpec.MaxFailedIndexes)) + } + if appliedFeastJobSpec.Parallelism != nil { + Expect(appliedFeastJobSpec.Parallelism).To(Equal(jobSpec.Parallelism)) + } + if appliedFeastJobSpec.PodFailurePolicy != nil { + Expect(appliedFeastJobSpec.PodFailurePolicy).To(Equal(jobSpec.PodFailurePolicy)) + } + if appliedFeastJobSpec.PodReplacementPolicy != nil { + Expect(appliedFeastJobSpec.PodReplacementPolicy).To(Equal(jobSpec.PodReplacementPolicy)) + } + if appliedFeastJobSpec.Suspend != nil { + Expect(appliedFeastJobSpec.Suspend).To(Equal(jobSpec.Suspend)) + } + if appliedFeastJobSpec.TTLSecondsAfterFinished != nil { + Expect(appliedFeastJobSpec.TTLSecondsAfterFinished).To(Equal(jobSpec.TTLSecondsAfterFinished)) + } + } +} + +func checkCronJobContainers(cronJobContainerConfigs *feastdevv1alpha1.CronJobContainerConfigs, podSpec corev1.PodSpec) { + Expect(cronJobContainerConfigs).NotTo(BeNil()) + Expect(cronJobContainerConfigs.Image).NotTo(BeNil()) + Expect(cronJobContainerConfigs.Commands).NotTo(BeEmpty()) + Expect(cronJobContainerConfigs.Commands).To(HaveLen(len(podSpec.InitContainers) + len(podSpec.Containers))) + + lastCmdIndex := len(cronJobContainerConfigs.Commands) - 1 + for i, cmd := range cronJobContainerConfigs.Commands { + if i < lastCmdIndex { + Expect(podSpec.InitContainers[i].Command).To(ContainElement(cmd)) + } else { + Expect(podSpec.Containers[0].Command).To(ContainElement(cmd)) + } + } +} diff --git a/infra/feast-operator/internal/controller/featurestore_controller_test.go b/infra/feast-operator/internal/controller/featurestore_controller_test.go index c73fbe1bff3..519e3e05618 100644 --- a/infra/feast-operator/internal/controller/featurestore_controller_test.go +++ b/infra/feast-operator/internal/controller/featurestore_controller_test.go @@ -457,7 +457,7 @@ var _ = Describe("FeatureStore Controller", func() { err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) - Expect(resource.Status.Conditions).To(HaveLen(3)) + Expect(resource.Status.Conditions).To(HaveLen(4)) cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ReadyType) Expect(cond).ToNot(BeNil()) @@ -482,6 +482,13 @@ var _ = Describe("FeatureStore Controller", func() { Expect(cond.Type).To(Equal(feastdevv1alpha1.ClientReadyType)) Expect(cond.Message).To(Equal(feastdevv1alpha1.ClientReadyMessage)) + cond = apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.CronJobReadyType) + Expect(cond).ToNot(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).To(Equal(feastdevv1alpha1.ReadyReason)) + Expect(cond.Type).To(Equal(feastdevv1alpha1.CronJobReadyType)) + Expect(cond.Message).To(Equal(feastdevv1alpha1.CronJobReadyMessage)) + Expect(resource.Status.Phase).To(Equal(feastdevv1alpha1.FailedPhase)) }) }) @@ -1260,7 +1267,7 @@ var _ = Describe("FeatureStore Controller", func() { err = k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) - Expect(resource.Status.Conditions).To(HaveLen(6)) + Expect(resource.Status.Conditions).To(HaveLen(7)) cond := apimeta.FindStatusCondition(resource.Status.Conditions, feastdevv1alpha1.ReadyType) Expect(cond).ToNot(BeNil()) diff --git a/infra/feast-operator/internal/controller/services/cronjob.go b/infra/feast-operator/internal/controller/services/cronjob.go new file mode 100644 index 00000000000..f15200d22ec --- /dev/null +++ b/infra/feast-operator/internal/controller/services/cronjob.go @@ -0,0 +1,289 @@ +package services + +import ( + "os" + "strconv" + + feastdevv1alpha1 "github.com/feast-dev/feast/infra/feast-operator/api/v1alpha1" + "github.com/feast-dev/feast/infra/feast-operator/internal/controller/handler" + appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +func (feast *FeastServices) deployCronJob() error { + if err := feast.createCronJobRole(); err != nil { + return feast.setFeastServiceCondition(err, CronJobFeastType) + } + if err := feast.createCronJobRoleBinding(); err != nil { + return feast.setFeastServiceCondition(err, CronJobFeastType) + } + if err := feast.createCronJob(); err != nil { + return feast.setFeastServiceCondition(err, CronJobFeastType) + } + return feast.setFeastServiceCondition(nil, CronJobFeastType) +} + +func (feast *FeastServices) createCronJob() error { + logger := log.FromContext(feast.Handler.Context) + cronJob := feast.initCronJob() + if op, err := controllerutil.CreateOrUpdate(feast.Handler.Context, feast.Handler.Client, cronJob, controllerutil.MutateFn(func() error { + return feast.setCronJob(cronJob) + })); err != nil { + return err + } else if op == controllerutil.OperationResultCreated || op == controllerutil.OperationResultUpdated { + logger.Info("Successfully reconciled", "CronJob", cronJob.Name, "operation", op) + } + + return nil +} + +func (feast *FeastServices) initCronJob() *batchv1.CronJob { + cronJob := &batchv1.CronJob{ + ObjectMeta: feast.GetObjectMeta(), + } + cronJob.SetGroupVersionKind(batchv1.SchemeGroupVersion.WithKind("CronJob")) + + return cronJob +} + +func (feast *FeastServices) setCronJob(cronJob *batchv1.CronJob) error { + appliedCronJob := feast.Handler.FeatureStore.Status.Applied.CronJob + cronJob.Labels = feast.getFeastTypeLabels(CronJobFeastType) + cronJob.Spec = batchv1.CronJobSpec{ + Schedule: appliedCronJob.Schedule, + JobTemplate: batchv1.JobTemplateSpec{ + Spec: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + Spec: feast.getCronJobPodSpec(), + }, + }, + }, + } + if appliedCronJob.Suspend != nil { + cronJob.Spec.Suspend = appliedCronJob.Suspend + } + if len(appliedCronJob.ConcurrencyPolicy) > 0 { + cronJob.Spec.ConcurrencyPolicy = appliedCronJob.ConcurrencyPolicy + } + if appliedCronJob.TimeZone != nil { + cronJob.Spec.TimeZone = appliedCronJob.TimeZone + } + if appliedCronJob.StartingDeadlineSeconds != nil { + cronJob.Spec.StartingDeadlineSeconds = appliedCronJob.StartingDeadlineSeconds + } + if appliedCronJob.SuccessfulJobsHistoryLimit != nil { + cronJob.Spec.SuccessfulJobsHistoryLimit = appliedCronJob.SuccessfulJobsHistoryLimit + } + if appliedCronJob.FailedJobsHistoryLimit != nil { + cronJob.Spec.FailedJobsHistoryLimit = appliedCronJob.FailedJobsHistoryLimit + } + + appliedJobSpec := appliedCronJob.JobSpec + if appliedJobSpec != nil { + jobSpec := &cronJob.Spec.JobTemplate.Spec + + if appliedJobSpec.ActiveDeadlineSeconds != nil { + jobSpec.ActiveDeadlineSeconds = appliedJobSpec.ActiveDeadlineSeconds + } + if appliedJobSpec.BackoffLimit != nil { + jobSpec.BackoffLimit = appliedJobSpec.BackoffLimit + } + if appliedJobSpec.BackoffLimitPerIndex != nil { + jobSpec.BackoffLimitPerIndex = appliedJobSpec.BackoffLimitPerIndex + } + if appliedJobSpec.CompletionMode != nil { + jobSpec.CompletionMode = appliedJobSpec.CompletionMode + } + if appliedJobSpec.Completions != nil { + jobSpec.Completions = appliedJobSpec.Completions + } + if appliedJobSpec.MaxFailedIndexes != nil { + jobSpec.MaxFailedIndexes = appliedJobSpec.MaxFailedIndexes + } + if appliedJobSpec.Parallelism != nil { + jobSpec.Parallelism = appliedJobSpec.Parallelism + } + if appliedJobSpec.PodFailurePolicy != nil { + jobSpec.PodFailurePolicy = appliedJobSpec.PodFailurePolicy + } + if appliedJobSpec.PodReplacementPolicy != nil { + jobSpec.PodReplacementPolicy = appliedJobSpec.PodReplacementPolicy + } + if appliedJobSpec.Suspend != nil { + jobSpec.Suspend = appliedJobSpec.Suspend + } + if appliedJobSpec.TTLSecondsAfterFinished != nil { + jobSpec.TTLSecondsAfterFinished = appliedJobSpec.TTLSecondsAfterFinished + } + } + feast.Handler.FeatureStore.Status.CronJob = cronJob.Name + return controllerutil.SetControllerReference(feast.Handler.FeatureStore, cronJob, feast.Handler.Scheme) +} + +func (feast *FeastServices) getCronJobPodSpec() corev1.PodSpec { + podSpec := corev1.PodSpec{ + ServiceAccountName: feast.initFeastSA().Name, + RestartPolicy: corev1.RestartPolicyNever, + } + feast.setCronJobContainers(&podSpec) + return podSpec +} + +func (feast *FeastServices) setCronJobContainers(podSpec *corev1.PodSpec) { + appliedCronJob := feast.Handler.FeatureStore.Status.Applied.CronJob + lastCmdIndex := len(appliedCronJob.ContainerConfigs.Commands) - 1 + for i, cronJobCmd := range appliedCronJob.ContainerConfigs.Commands { + containerName := handler.FeastPrefix + strconv.Itoa(i) + if i < lastCmdIndex { + podSpec.InitContainers = append(podSpec.InitContainers, feast.getCronJobContainer(containerName, cronJobCmd)) + } else { + podSpec.Containers = append(podSpec.Containers, feast.getCronJobContainer(containerName, cronJobCmd)) + } + } +} + +func (feast *FeastServices) getCronJobContainer(containerName, cronJobCmd string) corev1.Container { + return *getContainer( + containerName, + "", + []string{ + "kubectl", "exec", "deploy/" + feast.initFeastDeploy().Name, "-i", + "--", + "bash", "-c", cronJobCmd, + }, + feast.Handler.FeatureStore.Status.Applied.CronJob.ContainerConfigs.ContainerConfigs, + "", + ) +} + +func (feast *FeastServices) createCronJobRole() error { + logger := log.FromContext(feast.Handler.Context) + role := feast.initCronJobRole() + if op, err := controllerutil.CreateOrUpdate(feast.Handler.Context, feast.Handler.Client, role, controllerutil.MutateFn(func() error { + return feast.setCronJobRole(role) + })); err != nil { + return err + } else if op == controllerutil.OperationResultCreated || op == controllerutil.OperationResultUpdated { + logger.Info("Successfully reconciled", "Role", role.Name, "operation", op) + } + + return nil +} + +func (feast *FeastServices) initCronJobRole() *rbacv1.Role { + role := &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{Name: feast.getCronJobRoleName(), Namespace: feast.Handler.FeatureStore.Namespace}, + } + role.SetGroupVersionKind(rbacv1.SchemeGroupVersion.WithKind("Role")) + + return role +} + +func (feast *FeastServices) setCronJobRole(role *rbacv1.Role) error { + role.Labels = feast.getFeastTypeLabels(CronJobFeastType) + role.Rules = []rbacv1.PolicyRule{ + { + APIGroups: []string{appsv1.GroupName}, + Resources: []string{"deployments"}, + ResourceNames: []string{feast.initFeastDeploy().Name}, + Verbs: []string{"get"}, + }, + { + APIGroups: []string{corev1.GroupName}, + Resources: []string{"pods"}, + Verbs: []string{"list"}, + }, + { + APIGroups: []string{corev1.GroupName}, + Resources: []string{"pods/exec"}, + Verbs: []string{"create"}, + }, + } + + return controllerutil.SetControllerReference(feast.Handler.FeatureStore, role, feast.Handler.Scheme) +} + +func (feast *FeastServices) createCronJobRoleBinding() error { + logger := log.FromContext(feast.Handler.Context) + roleBinding := feast.initCronJobRoleBinding() + if op, err := controllerutil.CreateOrUpdate(feast.Handler.Context, feast.Handler.Client, roleBinding, controllerutil.MutateFn(func() error { + return feast.setCronJobRoleBinding(roleBinding) + })); err != nil { + return err + } else if op == controllerutil.OperationResultCreated || op == controllerutil.OperationResultUpdated { + logger.Info("Successfully reconciled", "RoleBinding", roleBinding.Name, "operation", op) + } + + return nil +} + +func (feast *FeastServices) initCronJobRoleBinding() *rbacv1.RoleBinding { + roleBinding := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{Name: feast.getCronJobRoleName(), Namespace: feast.Handler.FeatureStore.Namespace}, + } + roleBinding.SetGroupVersionKind(rbacv1.SchemeGroupVersion.WithKind("RoleBinding")) + + return roleBinding +} + +func (feast *FeastServices) setCronJobRoleBinding(roleBinding *rbacv1.RoleBinding) error { + roleBinding.Labels = feast.getFeastTypeLabels(CronJobFeastType) + roleBinding.Subjects = []rbacv1.Subject{{ + Kind: rbacv1.ServiceAccountKind, + Name: feast.initFeastSA().Name, + Namespace: feast.Handler.FeatureStore.Namespace, + }} + roleBinding.RoleRef = rbacv1.RoleRef{ + APIGroup: rbacv1.GroupName, + Kind: "Role", + Name: feast.getCronJobRoleName(), + } + + return controllerutil.SetControllerReference(feast.Handler.FeatureStore, roleBinding, feast.Handler.Scheme) +} + +func (feast *FeastServices) getCronJobRoleName() string { + return GetFeastServiceName(feast.Handler.FeatureStore, CronJobFeastType) +} + +// defaults to a CronJob configuration that will never run. this default Job can be executed manually, however. +// e.g. kubectl create job --from=cronjob/feast-sample feast-sample-job +func setDefaultCronJobConfigs(feastCronJob *feastdevv1alpha1.FeastCronJob) { + if len(feastCronJob.Schedule) == 0 { + feastCronJob.Schedule = "@yearly" + if feastCronJob.Suspend == nil { + feastCronJob.Suspend = boolPtr(true) + } + if len(feastCronJob.ConcurrencyPolicy) == 0 { + feastCronJob.ConcurrencyPolicy = batchv1.ReplaceConcurrent + } + if feastCronJob.StartingDeadlineSeconds == nil { + feastCronJob.StartingDeadlineSeconds = int64Ptr(5) + } + } + if feastCronJob.ContainerConfigs == nil { + feastCronJob.ContainerConfigs = &feastdevv1alpha1.CronJobContainerConfigs{} + } + if feastCronJob.ContainerConfigs.Image == nil { + feastCronJob.ContainerConfigs.Image = getCronJobImage() + } + if len(feastCronJob.ContainerConfigs.Commands) == 0 { + feastCronJob.ContainerConfigs.Commands = []string{ + "feast apply", + "feast materialize-incremental $(date -u +'%Y-%m-%dT%H:%M:%S')", + } + } +} + +func getCronJobImage() *string { + if img, exists := os.LookupEnv(cronJobImageVar); exists { + return &img + } + + return &DefaultCronJobImage +} diff --git a/infra/feast-operator/internal/controller/services/services.go b/infra/feast-operator/internal/controller/services/services.go index a9a6e3c687b..46d1d31d875 100644 --- a/infra/feast-operator/internal/controller/services/services.go +++ b/infra/feast-operator/internal/controller/services/services.go @@ -136,6 +136,9 @@ func (feast *FeastServices) Deploy() error { if err := feast.deployClient(); err != nil { return err } + if err := feast.deployCronJob(); err != nil { + return err + } return nil } @@ -384,43 +387,33 @@ func (feast *FeastServices) setContainers(podSpec *corev1.PodSpec) error { func (feast *FeastServices) setContainer(containers *[]corev1.Container, feastType FeastServiceType, fsYamlB64 string) { if serverConfigs := feast.getServerConfigs(feastType); serverConfigs != nil { - defaultCtrConfigs := serverConfigs.ContainerConfigs.DefaultCtrConfigs + name := string(feastType) + workingDir := feast.getFeatureRepoDir() + cmd := feast.getContainerCommand(feastType) + container := getContainer(name, workingDir, cmd, serverConfigs.ContainerConfigs, fsYamlB64) tls := feast.getTlsConfigs(feastType) probeHandler := getProbeHandler(feastType, tls) - container := &corev1.Container{ - Name: string(feastType), - Image: *defaultCtrConfigs.Image, - WorkingDir: feast.getFeatureRepoDir(), - Command: feast.getContainerCommand(feastType), - Ports: []corev1.ContainerPort{ - { - Name: string(feastType), - ContainerPort: getTargetPort(feastType, tls), - Protocol: corev1.ProtocolTCP, - }, - }, - Env: []corev1.EnvVar{ - { - Name: TmpFeatureStoreYamlEnvVar, - Value: fsYamlB64, - }, - }, - StartupProbe: &corev1.Probe{ - ProbeHandler: probeHandler, - PeriodSeconds: 3, - FailureThreshold: 40, - }, - LivenessProbe: &corev1.Probe{ - ProbeHandler: probeHandler, - PeriodSeconds: 20, - FailureThreshold: 6, - }, - ReadinessProbe: &corev1.Probe{ - ProbeHandler: probeHandler, - PeriodSeconds: 10, + container.Ports = []corev1.ContainerPort{ + { + Name: name, + ContainerPort: getTargetPort(feastType, tls), + Protocol: corev1.ProtocolTCP, }, } - applyOptionalCtrConfigs(container, serverConfigs.ContainerConfigs.OptionalCtrConfigs) + container.StartupProbe = &corev1.Probe{ + ProbeHandler: probeHandler, + PeriodSeconds: 3, + FailureThreshold: 40, + } + container.LivenessProbe = &corev1.Probe{ + ProbeHandler: probeHandler, + PeriodSeconds: 20, + FailureThreshold: 6, + } + container.ReadinessProbe = &corev1.Probe{ + ProbeHandler: probeHandler, + PeriodSeconds: 10, + } volumeMounts := feast.getVolumeMounts(feastType) if len(volumeMounts) > 0 { container.VolumeMounts = append(container.VolumeMounts, volumeMounts...) @@ -429,6 +422,26 @@ func (feast *FeastServices) setContainer(containers *[]corev1.Container, feastTy } } +func getContainer(name, workingDir string, cmd []string, containerConfigs feastdevv1alpha1.ContainerConfigs, fsYamlB64 string) *corev1.Container { + container := &corev1.Container{ + Name: name, + Command: cmd, + } + if len(workingDir) > 0 { + container.WorkingDir = workingDir + } + if len(fsYamlB64) > 0 { + container.Env = []corev1.EnvVar{ + { + Name: TmpFeatureStoreYamlEnvVar, + Value: fsYamlB64, + }, + } + } + applyCtrConfigs(container, containerConfigs) + return container +} + func (feast *FeastServices) mountUserDefinedVolumes(podSpec *corev1.PodSpec) { var volumes []corev1.Volume if feast.Handler.FeatureStore.Status.Applied.Services != nil { @@ -870,18 +883,20 @@ func (feast *FeastServices) initRoute(feastType FeastServiceType) *routev1.Route return route } -func applyOptionalCtrConfigs(container *corev1.Container, optionalConfigs feastdevv1alpha1.OptionalCtrConfigs) { - if optionalConfigs.Env != nil { - container.Env = envOverride(container.Env, *optionalConfigs.Env) +func applyCtrConfigs(container *corev1.Container, containerConfigs feastdevv1alpha1.ContainerConfigs) { + container.Image = *containerConfigs.DefaultCtrConfigs.Image + // apply optional container configs + if containerConfigs.OptionalCtrConfigs.Env != nil { + container.Env = envOverride(container.Env, *containerConfigs.OptionalCtrConfigs.Env) } - if optionalConfigs.EnvFrom != nil { - container.EnvFrom = *optionalConfigs.EnvFrom + if containerConfigs.OptionalCtrConfigs.EnvFrom != nil { + container.EnvFrom = *containerConfigs.OptionalCtrConfigs.EnvFrom } - if optionalConfigs.ImagePullPolicy != nil { - container.ImagePullPolicy = *optionalConfigs.ImagePullPolicy + if containerConfigs.OptionalCtrConfigs.ImagePullPolicy != nil { + container.ImagePullPolicy = *containerConfigs.OptionalCtrConfigs.ImagePullPolicy } - if optionalConfigs.Resources != nil { - container.Resources = *optionalConfigs.Resources + if containerConfigs.OptionalCtrConfigs.Resources != nil { + container.Resources = *containerConfigs.OptionalCtrConfigs.Resources } } diff --git a/infra/feast-operator/internal/controller/services/services_types.go b/infra/feast-operator/internal/controller/services/services_types.go index c846725f3ba..3cc34ecd85a 100644 --- a/infra/feast-operator/internal/controller/services/services_types.go +++ b/infra/feast-operator/internal/controller/services/services_types.go @@ -27,6 +27,7 @@ import ( const ( TmpFeatureStoreYamlEnvVar = "TMP_FEATURE_STORE_YAML_BASE64" feastServerImageVar = "RELATED_IMAGE_FEATURE_SERVER" + cronJobImageVar = "RELATED_IMAGE_CRON_JOB" FeatureStoreYamlCmKey = "feature_store.yaml" EphemeralPath = "/feast-data" FeatureRepoDir = "feature_repo" @@ -49,12 +50,14 @@ const ( DefaultOnlineStorageRequest = "5Gi" DefaultRegistryStorageRequest = "5Gi" + AuthzFeastType FeastServiceType = "authorization" OfflineFeastType FeastServiceType = "offline" OnlineFeastType FeastServiceType = "online" RegistryFeastType FeastServiceType = "registry" UIFeastType FeastServiceType = "ui" ClientFeastType FeastServiceType = "client" ClientCaFeastType FeastServiceType = "client-ca" + CronJobFeastType FeastServiceType = "cronjob" OfflineRemoteConfigType OfflineConfigType = "remote" OfflineFilePersistenceDaskConfigType OfflineConfigType = "dask" @@ -88,6 +91,7 @@ const ( var ( DefaultImage = "quay.io/feastdev/feature-server:" + feastversion.FeastVersion + DefaultCronJobImage = "quay.io/openshift/origin-cli:4.17" DefaultReplicas = int32(1) DefaultPVCAccessModes = []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce} NameLabelKey = feastdevv1alpha1.GroupVersion.Group + "/name" @@ -169,7 +173,6 @@ var ( Reason: feastdevv1alpha1.UIFailedReason, }, }, - ClientFeastType: { metav1.ConditionTrue: { Type: feastdevv1alpha1.ClientReadyType, @@ -183,6 +186,19 @@ var ( Reason: feastdevv1alpha1.ClientFailedReason, }, }, + CronJobFeastType: { + metav1.ConditionTrue: { + Type: feastdevv1alpha1.CronJobReadyType, + Status: metav1.ConditionTrue, + Reason: feastdevv1alpha1.ReadyReason, + Message: feastdevv1alpha1.CronJobReadyMessage, + }, + metav1.ConditionFalse: { + Type: feastdevv1alpha1.CronJobReadyType, + Status: metav1.ConditionFalse, + Reason: feastdevv1alpha1.CronJobFailedReason, + }, + }, } OidcServerProperties = []OidcPropertyType{OidcClientId, OidcAuthDiscoveryUrl} diff --git a/infra/feast-operator/internal/controller/services/util.go b/infra/feast-operator/internal/controller/services/util.go index 41f3e837157..41f961e557b 100644 --- a/infra/feast-operator/internal/controller/services/util.go +++ b/infra/feast-operator/internal/controller/services/util.go @@ -181,6 +181,11 @@ func ApplyDefaultsToStatus(cr *feastdevv1alpha1.FeatureStore) { if services.UI != nil { setDefaultCtrConfigs(&services.UI.ContainerConfigs.DefaultCtrConfigs) } + + if applied.CronJob == nil { + applied.CronJob = &feastdevv1alpha1.FeastCronJob{} + } + setDefaultCronJobConfigs(applied.CronJob) } func setDefaultCtrConfigs(defaultConfigs *feastdevv1alpha1.DefaultCtrConfigs) { @@ -468,3 +473,11 @@ func getVolumeMountByType(feastType FeastServiceType, featureStore *feastdevv1al } return nil } + +func boolPtr(value bool) *bool { + return &value +} + +func int64Ptr(value int64) *int64 { + return &value +}