diff --git a/examples/operator-rbac/.gitignore b/examples/operator-rbac/.gitignore new file mode 100644 index 00000000000..ebfbe5382d8 --- /dev/null +++ b/examples/operator-rbac/.gitignore @@ -0,0 +1 @@ +client/feature_repo/feature_store.yaml diff --git a/examples/operator-rbac/03-uninstall.ipynb b/examples/operator-rbac/04-uninstall.ipynb similarity index 73% rename from examples/operator-rbac/03-uninstall.ipynb rename to examples/operator-rbac/04-uninstall.ipynb index f9c794c03f8..ac34297cdc0 100644 --- a/examples/operator-rbac/03-uninstall.ipynb +++ b/examples/operator-rbac/04-uninstall.ipynb @@ -11,14 +11,14 @@ { "metadata": {}, "cell_type": "markdown", - "source": "### Uninstall the Operator and all Feast related objects##", + "source": "### Uninstall the Operator and Feast Instance", "id": "1175f3d6c5ee9bf0" }, { "metadata": { "ExecuteTime": { - "end_time": "2025-03-05T19:09:52.349677Z", - "start_time": "2025-03-05T19:09:46.308482Z" + "end_time": "2025-03-14T14:45:22.053112Z", + "start_time": "2025-03-14T14:45:15.816729Z" } }, "cell_type": "code", @@ -50,19 +50,19 @@ ] } ], - "execution_count": 6 + "execution_count": 4 }, { "metadata": {}, "cell_type": "markdown", - "source": "## Uninstall Client Related Objects", + "source": "## Delete RoleBindings and ServiceAccounts\n", "id": "2a2aa884aeddfb99" }, { "metadata": { "ExecuteTime": { - "end_time": "2025-03-05T19:09:54.655575Z", - "start_time": "2025-03-05T19:09:53.553918Z" + "end_time": "2025-03-14T14:45:47.419179Z", + "start_time": "2025-03-14T14:45:46.325817Z" } }, "cell_type": "code", @@ -92,19 +92,54 @@ ] } ], - "execution_count": 7 + "execution_count": 5 }, { "metadata": {}, "cell_type": "markdown", - "source": "Ensure everything has been removed, or is in the process of being terminated.", - "id": "638421caa8ff849e" + "source": "### Delete Client Example Deployments", + "id": "fa7a79763774f770" }, { "metadata": { "ExecuteTime": { - "end_time": "2025-03-05T19:09:59.868383Z", - "start_time": "2025-03-05T19:09:59.611048Z" + "end_time": "2025-03-14T14:46:05.998191Z", + "start_time": "2025-03-14T14:46:05.344334Z" + } + }, + "cell_type": "code", + "source": [ + "!kubectl delete -f client/admin_user_deployment.yaml\n", + "!kubectl delete -f client/readonly_user_deployment.yaml\n", + "!kubectl delete -f client/unauthorized_user_deployment.yaml\n", + "!kubectl delete configmap client-feature-repo-config -n feast" + ], + "id": "7bc23b3eb0153c75", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "deployment.apps \"client-admin-user\" deleted\r\n", + "deployment.apps \"client-readonly-user\" deleted\r\n", + "deployment.apps \"client-unauthorized-user\" deleted\r\n", + "Error from server (NotFound): configmaps \"client-feature-repo-config\" not found\r\n" + ] + } + ], + "execution_count": 6 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "### Validate all Objects Removed from Namespace and Delete the Namespace", + "id": "ce8ef7c832d146dd" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-14T14:46:14.626703Z", + "start_time": "2025-03-14T14:46:14.429984Z" } }, "cell_type": "code", @@ -119,13 +154,13 @@ ] } ], - "execution_count": 8 + "execution_count": 7 }, { "metadata": { "ExecuteTime": { - "end_time": "2025-03-05T19:10:07.846749Z", - "start_time": "2025-03-05T19:10:02.561070Z" + "end_time": "2025-03-14T14:46:26.127988Z", + "start_time": "2025-03-14T14:46:20.865605Z" } }, "cell_type": "code", @@ -140,15 +175,7 @@ ] } ], - "execution_count": 9 - }, - { - "metadata": {}, - "cell_type": "code", - "outputs": [], - "execution_count": null, - "source": "", - "id": "10707783148c5f8d" + "execution_count": 8 } ], "metadata": { diff --git a/examples/operator-rbac/1-setup-operator-rbac.ipynb b/examples/operator-rbac/1-setup-operator-rbac.ipynb index 69cc285a01c..2a32d5c4453 100644 --- a/examples/operator-rbac/1-setup-operator-rbac.ipynb +++ b/examples/operator-rbac/1-setup-operator-rbac.ipynb @@ -9,7 +9,7 @@ "\n", "This demo provides a reference implementation of a runbook on how to enable Role-Based Access Control (RBAC) for Feast using the Feast Operator with the Kubernetes authentication type. This serves as useful reference material for a cluster admin / MLOps engineer.\n", "\n", - "The demo steps include deploying the Feast Operator, creating Feast instances with server components (registry, offline store, online store), and Feast client testing locally. The goal is to ensure secure access control for Feast instances deployed by the Feast Operator.\n", + "The demo steps include deploying the Feast Operator, creating Feast instances with server components (registry, offline store, online store), and Feast client testing locally and fom the Kubernetes. The goal is to ensure secure access control for Feast instances deployed by the Feast Operator.\n", " \n", "Please read these reference documents for understanding the Feast RBAC framework.\n", "- [RBAC Architecture](https://docs.feast.dev/v/master/getting-started/architecture/rbac) \n", @@ -18,8 +18,8 @@ ] }, { - "metadata": {}, "cell_type": "markdown", + "metadata": {}, "source": [ "## Deployment Architecture\n", "In this notebook, we will deploy a distributed topology of Feast services, which includes:\n", @@ -33,8 +33,8 @@ ] }, { - "metadata": {}, "cell_type": "markdown", + "metadata": {}, "source": [ "## Prerequisites\n", "* Kubernetes Cluster\n", @@ -42,8 +42,8 @@ ] }, { - "metadata": {}, "cell_type": "markdown", + "metadata": {}, "source": [ "## Install Prerequisites\n", "The following commands install and configure all the prerequisites on a MacOS environment. You can find the\n", @@ -60,13 +60,13 @@ ] }, { + "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-03-06T18:27:31.474254Z", - "start_time": "2025-03-06T18:27:31.012088Z" + "end_time": "2025-03-14T14:47:46.558319Z", + "start_time": "2025-03-14T14:47:46.117084Z" } }, - "cell_type": "code", "source": [ "!kubectl create ns feast\n", "!kubectl config set-context --current --namespace feast" @@ -84,34 +84,38 @@ "execution_count": 1 }, { - "metadata": {}, "cell_type": "markdown", - "source": "Validate the cluster setup:" + "metadata": {}, + "source": [ + "Validate the cluster setup:" + ] }, { + "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-03-06T18:32:23.198122Z", - "start_time": "2025-03-06T18:32:22.930547Z" + "end_time": "2025-03-14T14:47:52.996466Z", + "start_time": "2025-03-14T14:47:52.827264Z" } }, - "cell_type": "code", - "source": "!kubectl get ns feast", + "source": [ + "!kubectl get ns feast" + ], "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "NAME STATUS AGE\r\n", - "feast Active 4m52s\r\n" + "feast Active 6s\r\n" ] } ], "execution_count": 2 }, { - "metadata": {}, "cell_type": "markdown", + "metadata": {}, "source": [ "## Feast Admin Steps:\n", "Feast Admins or MLOps Engineers may require Kubernetes Cluster Admin roles when working with OpenShift or Kubernetes clusters. Below is the list of steps Required to set up Feast RBAC with the Operator by an Admin or MLOps Engineer.\n", @@ -126,13 +130,13 @@ ] }, { + "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-03-06T18:32:40.721042Z", - "start_time": "2025-03-06T18:32:28.484245Z" + "end_time": "2025-03-14T14:48:42.482143Z", + "start_time": "2025-03-14T14:48:29.671332Z" } }, - "cell_type": "code", "source": [ "## Use this install command from a stable branch \n", "!kubectl apply -f ../../infra/feast-operator/dist/install.yaml\n", @@ -180,8 +184,8 @@ "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-03-06T18:34:39.847211Z", - "start_time": "2025-03-06T18:34:39.378680Z" + "end_time": "2025-03-14T14:48:55.797775Z", + "start_time": "2025-03-14T14:48:55.453327Z" } }, "source": [ @@ -231,8 +235,8 @@ "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-03-06T18:35:05.202176Z", - "start_time": "2025-03-06T18:35:02.498106Z" + "end_time": "2025-03-14T14:49:24.996009Z", + "start_time": "2025-03-14T14:49:24.507177Z" } }, "source": [ @@ -245,24 +249,24 @@ "output_type": "stream", "text": [ "NAME READY STATUS RESTARTS AGE\r\n", - "pod/feast-sample-kubernetes-auth-774f6df8df-95nc6 0/4 Running 0 22s\r\n", + "pod/feast-sample-kubernetes-auth-775774f6f6-t8q4x 4/4 Running 0 28s\r\n", "\r\n", "NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE\r\n", - "service/feast-sample-kubernetes-auth-offline ClusterIP 10.96.38.230 80/TCP 22s\r\n", - "service/feast-sample-kubernetes-auth-online ClusterIP 10.96.140.194 80/TCP 22s\r\n", - "service/feast-sample-kubernetes-auth-registry ClusterIP 10.96.140.31 80/TCP 22s\r\n", - "service/feast-sample-kubernetes-auth-ui ClusterIP 10.96.26.21 80/TCP 22s\r\n", + "service/feast-sample-kubernetes-auth-offline ClusterIP 10.96.98.188 80/TCP 28s\r\n", + "service/feast-sample-kubernetes-auth-online ClusterIP 10.96.181.225 80/TCP 28s\r\n", + "service/feast-sample-kubernetes-auth-registry ClusterIP 10.96.95.48 80/TCP 28s\r\n", + "service/feast-sample-kubernetes-auth-ui ClusterIP 10.96.243.121 80/TCP 28s\r\n", "\r\n", "NAME READY UP-TO-DATE AVAILABLE AGE\r\n", - "deployment.apps/feast-sample-kubernetes-auth 0/1 1 0 22s\r\n", + "deployment.apps/feast-sample-kubernetes-auth 1/1 1 1 28s\r\n", "\r\n", "NAME DESIRED CURRENT READY AGE\r\n", - "replicaset.apps/feast-sample-kubernetes-auth-774f6df8df 1 1 0 22s\r\n", + "replicaset.apps/feast-sample-kubernetes-auth-775774f6f6 1 1 1 28s\r\n", "deployment.apps/feast-sample-kubernetes-auth condition met\r\n" ] } ], - "execution_count": 5 + "execution_count": 6 }, { "cell_type": "markdown", @@ -275,8 +279,8 @@ "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-03-06T18:35:55.728523Z", - "start_time": "2025-03-06T18:35:55.452894Z" + "end_time": "2025-03-14T14:49:32.148636Z", + "start_time": "2025-03-14T14:49:31.981954Z" } }, "source": [ @@ -288,28 +292,28 @@ "output_type": "stream", "text": [ "NAME STATUS AGE\r\n", - "sample-kubernetes-auth Ready 76s\r\n" + "sample-kubernetes-auth Ready 37s\r\n" ] } ], - "execution_count": 6 + "execution_count": 7 }, { - "metadata": {}, "cell_type": "markdown", + "metadata": {}, "source": [ "## Configure the RBAC Permissions\n", "As we have created Kubernetes roles in FeatureStore CR to manage access control for Feast objects, the Python script `permissions_apply.py` will apply these roles to configure permissions. See the detailed code example below with comments." ] }, { + "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-03-06T18:37:17.062072Z", - "start_time": "2025-03-06T18:37:16.930026Z" + "end_time": "2025-03-14T14:49:39.824642Z", + "start_time": "2025-03-14T14:49:39.695940Z" } }, - "cell_type": "code", "source": [ "#view the permissions \n", "!cat permissions_apply.py" @@ -349,29 +353,29 @@ ] } ], - "execution_count": 7 + "execution_count": 8 }, { + "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-03-06T18:37:31.662484Z", - "start_time": "2025-03-06T18:37:31.139869Z" + "end_time": "2025-03-14T14:49:45.670402Z", + "start_time": "2025-03-14T14:49:45.248755Z" } }, - "cell_type": "code", "source": [ "# Copy the Permissions to the pods under feature_repo directory\n", "!kubectl cp permissions_apply.py $(kubectl get pods -l 'feast.dev/name=sample-kubernetes-auth' -ojsonpath=\"{.items[*].metadata.name}\"):/feast-data/feast_rbac/feature_repo -c online" ], "outputs": [], - "execution_count": 8 + "execution_count": 9 }, { "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-03-06T18:37:38.003082Z", - "start_time": "2025-03-06T18:37:37.662378Z" + "end_time": "2025-03-14T14:49:51.754691Z", + "start_time": "2025-03-14T14:49:51.466463Z" } }, "source": [ @@ -399,22 +403,26 @@ ] } ], - "execution_count": 9 + "execution_count": 10 }, { - "metadata": {}, "cell_type": "markdown", - "source": "## Apply the Permissions and Feast Object to Registry" + "metadata": {}, + "source": [ + "## Apply the Permissions and Feast Object to Registry" + ] }, { + "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-03-06T18:37:56.131390Z", - "start_time": "2025-03-06T18:37:45.483916Z" + "end_time": "2025-03-14T14:50:07.432351Z", + "start_time": "2025-03-14T14:49:56.731316Z" } }, - "cell_type": "code", - "source": "!kubectl exec deploy/feast-sample-kubernetes-auth -itc online -- feast apply", + "source": [ + "!kubectl exec deploy/feast-sample-kubernetes-auth -itc online -- feast apply" + ], "outputs": [ { "name": "stdout", @@ -422,8 +430,6 @@ "text": [ ": MADV_DONTNEED does not work (memset will be used instead)\r\n", ": (This is the expected behaviour if you are running under QEMU)\r\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'.\r\n", - " DUMMY_ENTITY = Entity(\r\n", "/opt/app-root/lib64/python3.11/site-packages/pydantic/_internal/_fields.py:192: UserWarning: Field name \"vector_enabled\" in \"SqliteOnlineStoreConfig\" shadows an attribute in parent \"VectorStoreConfig\"\r\n", " warnings.warn(\r\n", "/opt/app-root/lib64/python3.11/site-packages/pydantic/_internal/_fields.py:192: UserWarning: Field name \"vector_len\" in \"SqliteOnlineStoreConfig\" shadows an attribute in parent \"VectorStoreConfig\"\r\n", @@ -431,19 +437,19 @@ "/feast-data/feast_rbac/feature_repo/example_repo.py:27: DeprecationWarning: Entity value_type will be mandatory in the next release. Please specify a value_type for entity 'driver'.\r\n", " driver = Entity(name=\"driver\", join_keys=[\"driver_id\"])\r\n", "Applying changes for project feast_rbac\r\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\r\n", + "/opt/app-root/lib64/python3.11/site-packages/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\r\n", " warnings.warn(\r\n", "Created project \u001B[1m\u001B[32mfeast_rbac\u001B[0m\r\n", "Created entity \u001B[1m\u001B[32mdriver\u001B[0m\r\n", "Created feature view \u001B[1m\u001B[32mdriver_hourly_stats\u001B[0m\r\n", "Created feature view \u001B[1m\u001B[32mdriver_hourly_stats_fresh\u001B[0m\r\n", - "Created on demand feature view \u001B[1m\u001B[32mtransformed_conv_rate\u001B[0m\r\n", "Created on demand feature view \u001B[1m\u001B[32mtransformed_conv_rate_fresh\u001B[0m\r\n", - "Created feature service \u001B[1m\u001B[32mdriver_activity_v2\u001B[0m\r\n", + "Created on demand feature view \u001B[1m\u001B[32mtransformed_conv_rate\u001B[0m\r\n", "Created feature service \u001B[1m\u001B[32mdriver_activity_v1\u001B[0m\r\n", + "Created feature service \u001B[1m\u001B[32mdriver_activity_v2\u001B[0m\r\n", "Created feature service \u001B[1m\u001B[32mdriver_activity_v3\u001B[0m\r\n", - "Created permission \u001B[1m\u001B[32mfeast_admin_permission\u001B[0m\r\n", "Created permission \u001B[1m\u001B[32mfeast_user_permission\u001B[0m\r\n", + "Created permission \u001B[1m\u001B[32mfeast_admin_permission\u001B[0m\r\n", "\r\n", "Created sqlite table \u001B[1m\u001B[32mfeast_rbac_driver_hourly_stats_fresh\u001B[0m\r\n", "Created sqlite table \u001B[1m\u001B[32mfeast_rbac_driver_hourly_stats\u001B[0m\r\n", @@ -451,19 +457,21 @@ ] } ], - "execution_count": 10 + "execution_count": 11 }, { "cell_type": "markdown", "metadata": {}, - "source": "**List the applied permission details permissions on Feast Resources.**" + "source": [ + "**List the applied permission details permissions on Feast Resources.**" + ] }, { "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-03-06T18:38:45.881715Z", - "start_time": "2025-03-06T18:38:04.170364Z" + "end_time": "2025-03-14T14:50:59.527146Z", + "start_time": "2025-03-14T14:50:15.549290Z" } }, "source": [ @@ -479,8 +487,6 @@ "text": [ ": MADV_DONTNEED does not work (memset will be used instead)\r\n", ": (This is the expected behaviour if you are running under QEMU)\r\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'.\r\n", - " DUMMY_ENTITY = Entity(\r\n", "/opt/app-root/lib64/python3.11/site-packages/pydantic/_internal/_fields.py:192: UserWarning: Field name \"vector_enabled\" in \"SqliteOnlineStoreConfig\" shadows an attribute in parent \"VectorStoreConfig\"\r\n", " warnings.warn(\r\n", "/opt/app-root/lib64/python3.11/site-packages/pydantic/_internal/_fields.py:192: UserWarning: Field name \"vector_len\" in \"SqliteOnlineStoreConfig\" shadows an attribute in parent \"VectorStoreConfig\"\r\n", @@ -494,24 +500,11 @@ "+--------------+\r\n", ": MADV_DONTNEED does not work (memset will be used instead)\r\n", ": (This is the expected behaviour if you are running under QEMU)\r\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'.\r\n", - " DUMMY_ENTITY = Entity(\r\n", "/opt/app-root/lib64/python3.11/site-packages/pydantic/_internal/_fields.py:192: UserWarning: Field name \"vector_enabled\" in \"SqliteOnlineStoreConfig\" shadows an attribute in parent \"VectorStoreConfig\"\r\n", " warnings.warn(\r\n", "/opt/app-root/lib64/python3.11/site-packages/pydantic/_internal/_fields.py:192: UserWarning: Field name \"vector_len\" in \"SqliteOnlineStoreConfig\" shadows an attribute in parent \"VectorStoreConfig\"\r\n", " warnings.warn(\r\n", "NAME TYPES NAME_PATTERNS ACTIONS ROLES REQUIRED_TAGS\r\n", - "feast_admin_permission Project - CREATE feast-writer -\r\n", - " FeatureView DESCRIBE\r\n", - " OnDemandFeatureView UPDATE\r\n", - " BatchFeatureView DELETE\r\n", - " StreamFeatureView READ_ONLINE\r\n", - " Entity READ_OFFLINE\r\n", - " FeatureService WRITE_ONLINE\r\n", - " DataSource WRITE_OFFLINE\r\n", - " ValidationReference\r\n", - " SavedDataset\r\n", - " Permission\r\n", "feast_user_permission Project - DESCRIBE feast-reader -\r\n", " FeatureView READ_OFFLINE\r\n", " OnDemandFeatureView READ_ONLINE\r\n", @@ -523,10 +516,19 @@ " ValidationReference\r\n", " SavedDataset\r\n", " Permission\r\n", + "feast_admin_permission Project - CREATE feast-writer -\r\n", + " FeatureView DESCRIBE\r\n", + " OnDemandFeatureView UPDATE\r\n", + " BatchFeatureView DELETE\r\n", + " StreamFeatureView READ_ONLINE\r\n", + " Entity READ_OFFLINE\r\n", + " FeatureService WRITE_ONLINE\r\n", + " DataSource WRITE_OFFLINE\r\n", + " ValidationReference\r\n", + " SavedDataset\r\n", + " Permission\r\n", ": MADV_DONTNEED does not work (memset will be used instead)\r\n", ": (This is the expected behaviour if you are running under QEMU)\r\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'.\r\n", - " DUMMY_ENTITY = Entity(\r\n", "/opt/app-root/lib64/python3.11/site-packages/pydantic/_internal/_fields.py:192: UserWarning: Field name \"vector_enabled\" in \"SqliteOnlineStoreConfig\" shadows an attribute in parent \"VectorStoreConfig\"\r\n", " warnings.warn(\r\n", "/opt/app-root/lib64/python3.11/site-packages/pydantic/_internal/_fields.py:192: UserWarning: Field name \"vector_len\" in \"SqliteOnlineStoreConfig\" shadows an attribute in parent \"VectorStoreConfig\"\r\n", @@ -559,13 +561,11 @@ " roles:\r\n", " - feast-writer\r\n", "meta:\r\n", - " createdTimestamp: '2025-03-06T18:37:55.742625Z'\r\n", - " lastUpdatedTimestamp: '2025-03-06T18:37:55.742625Z'\r\n", + " createdTimestamp: '2025-03-14T14:50:06.978099Z'\r\n", + " lastUpdatedTimestamp: '2025-03-14T14:50:06.978099Z'\r\n", "\r\n", ": MADV_DONTNEED does not work (memset will be used instead)\r\n", ": (This is the expected behaviour if you are running under QEMU)\r\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'.\r\n", - " DUMMY_ENTITY = Entity(\r\n", "/opt/app-root/lib64/python3.11/site-packages/pydantic/_internal/_fields.py:192: UserWarning: Field name \"vector_enabled\" in \"SqliteOnlineStoreConfig\" shadows an attribute in parent \"VectorStoreConfig\"\r\n", " warnings.warn(\r\n", "/opt/app-root/lib64/python3.11/site-packages/pydantic/_internal/_fields.py:192: UserWarning: Field name \"vector_len\" in \"SqliteOnlineStoreConfig\" shadows an attribute in parent \"VectorStoreConfig\"\r\n", @@ -593,17 +593,17 @@ " roles:\r\n", " - feast-reader\r\n", "meta:\r\n", - " createdTimestamp: '2025-03-06T18:37:55.743643Z'\r\n", - " lastUpdatedTimestamp: '2025-03-06T18:37:55.743643Z'\r\n", + " createdTimestamp: '2025-03-14T14:50:06.977346Z'\r\n", + " lastUpdatedTimestamp: '2025-03-14T14:50:06.977346Z'\r\n", "\r\n" ] } ], - "execution_count": 11 + "execution_count": 12 }, { - "metadata": {}, "cell_type": "markdown", + "metadata": {}, "source": [ "## Setting Up Service Account and RoleBinding \n", "The steps below will:\n", @@ -612,8 +612,8 @@ ] }, { - "metadata": {}, "cell_type": "markdown", + "metadata": {}, "source": [ "## Test Cases\n", "| User Type | ServiceAccount | RoleBinding Assigned | Expected Behavior in output |\n", @@ -624,23 +624,26 @@ ] }, { - "metadata": {}, "cell_type": "markdown", - "source": "" + "metadata": {}, + "source": [] }, { - "metadata": {}, "cell_type": "markdown", - "source": "### Setup Read-Only Feast User the ServiceAccount and Role Binding (serviceaccount: feast-user-sa, role: feast-reader)" + "metadata": {}, + "source": [ + "### Set Up a Read-Only Feast User \n", + "(ServiceAccount: feast-user-sa, Role: feast-reader)" + ] }, { + "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-03-06T18:42:04.122440Z", - "start_time": "2025-03-06T18:42:03.397214Z" + "end_time": "2025-03-14T14:55:47.715282Z", + "start_time": "2025-03-14T14:55:47.045535Z" } }, - "cell_type": "code", "source": [ "# Step 1: Create the ServiceAccount\n", "!echo \"Creating ServiceAccount: feast-user-sa\"\n", @@ -662,21 +665,24 @@ ] } ], - "execution_count": 12 + "execution_count": 13 }, { - "metadata": {}, "cell_type": "markdown", - "source": "### Setup Unauthorized Feast User (serviceaccount: feast-unauthorized-user-sa, role: None)" + "metadata": {}, + "source": [ + "### Set Up an Unauthorized Feast User\n", + "(ServiceAccount: feast-unauthorized-user-sa, Role: None)" + ] }, { + "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-03-06T18:42:07.992216Z", - "start_time": "2025-03-06T18:42:07.721628Z" + "end_time": "2025-03-14T14:55:53.079167Z", + "start_time": "2025-03-14T14:55:52.795933Z" } }, - "cell_type": "code", "source": [ "# Create the ServiceAccount (Without RoleBinding)\n", "!echo \"Creating Unauthorized ServiceAccount: feast-unauthorized-user-sa\"\n", @@ -692,21 +698,24 @@ ] } ], - "execution_count": 13 + "execution_count": 14 }, { - "metadata": {}, "cell_type": "markdown", - "source": "## Setup Test Admin Feast User (serviceaccount: feast-admin-sa, role: feast-writer)" + "metadata": {}, + "source": [ + "### Set Up a Test Admin Feast User\n", + "(ServiceAccount: feast-admin-sa, Role: feast-writer)" + ] }, { + "cell_type": "code", "metadata": { "ExecuteTime": { - "end_time": "2025-03-06T18:42:11.651408Z", - "start_time": "2025-03-06T18:42:11.097231Z" + "end_time": "2025-03-14T14:55:59.103855Z", + "start_time": "2025-03-14T14:55:58.548503Z" } }, - "cell_type": "code", "source": [ "# Create the ServiceAccount\n", "!echo \"Creating ServiceAccount: feast-admin-sa\"\n", @@ -728,12 +737,12 @@ ] } ], - "execution_count": 14 + "execution_count": 15 }, { - "metadata": {}, "cell_type": "markdown", - "source": "[Next Run Client notebook](./2-client.ipynb)" + "metadata": {}, + "source": "[Next: Client example from Pod](./2-client-rbac-test-pod.ipynb)" } ], "metadata": { @@ -752,7 +761,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.11" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/examples/operator-rbac/2-client-rbac-test-pod.ipynb b/examples/operator-rbac/2-client-rbac-test-pod.ipynb new file mode 100644 index 00000000000..19f3ecab95f --- /dev/null +++ b/examples/operator-rbac/2-client-rbac-test-pod.ipynb @@ -0,0 +1,499 @@ +{ + "cells": [ + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Feast Client with RBAC\n", + "\n", + "## Feast Kubernetes RBAC Authorization\n", + "\n", + "Feast **Role-Based Access Control (RBAC)** in Kubernetes relies on a **service account** for authentication. This applies both **within a Kubernetes pod** and for **external clients** accessing Feast\n", + "\n", + "In this example, Feast will automatically retrieve the Kubernetes ServiceAccount token from pod path:\n", + "```\n", + "/var/run/secrets/kubernetes.io/serviceaccount/token\n", + "```\n", + "This means:\n", + "- No manual configuration is needed inside a pod.\n", + "- The token is mounted automatically and used for authentication.\n", + "- Developer?User just need create the binding with role and service account accordingly.\n", + "\n", + "For more details, refer to the user guide: [Kubernetes RBAC Authorization](https://docs.feast.dev/master/getting-started/components/authz_manager#kubernetes-rbac-authorization). \n" + ], + "id": "bb0145c9c1f6ebcc" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Feature Store settings\n", + "**The Operator create client ConfigMap** containing the `feature_store.yaml `settings. We can retrieve it save it feature_repo folder." + ], + "id": "6590c081efb1fe3c" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-14T15:00:28.986653Z", + "start_time": "2025-03-14T15:00:28.670157Z" + } + }, + "cell_type": "code", + "source": [ + "!kubectl get configmap feast-sample-kubernetes-auth-client -n feast -o jsonpath='{.data.feature_store\\.yaml}' > client/feature_repo/feature_store.yaml\n", + "!cat client/feature_repo/feature_store.yaml" + ], + "id": "456fb4df46f32380", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "project: feast_rbac\r\n", + "provider: local\r\n", + "offline_store:\r\n", + " host: feast-sample-kubernetes-auth-offline.feast.svc.cluster.local\r\n", + " type: remote\r\n", + " port: 80\r\n", + "online_store:\r\n", + " path: http://feast-sample-kubernetes-auth-online.feast.svc.cluster.local:80\r\n", + " type: remote\r\n", + "registry:\r\n", + " path: feast-sample-kubernetes-auth-registry.feast.svc.cluster.local:80\r\n", + " registry_type: remote\r\n", + "auth:\r\n", + " type: kubernetes\r\n", + "entity_key_serialization_version: 3\r\n" + ] + } + ], + "execution_count": 20 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "**Create ConfigMap From Feature Repository** \n", + "We need feature_repo inside the container. let's create configmap from `feature_repo` contains the feature repository files, including `feature-store.yaml` and `test.py`. It will be mounted as a volume in the deployment for the client examples to test the script." + ], + "id": "84f73e09711bff9f" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-14T15:02:03.243912Z", + "start_time": "2025-03-14T15:02:02.804431Z" + } + }, + "cell_type": "code", + "source": [ + "!kubectl delete configmap client-feature-repo-config --ignore-not-found -n feast \n", + "!kubectl create configmap client-feature-repo-config --from-file=client/feature_repo -n feast" + ], + "id": "b840ac6ea3b95e90", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "configmap/client-feature-repo-config created\r\n" + ] + } + ], + "execution_count": 21 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "### Test Read-Only Feast User \n", + "\n", + "**Step 1: Deploy read-only user, we are using `serviceAccountName feast-user-sa` in deployment.**\n" + ], + "id": "84e3f83699b8d83" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-14T15:03:23.308812Z", + "start_time": "2025-03-14T15:03:22.868434Z" + } + }, + "cell_type": "code", + "source": [ + "# Create the deployment \n", + "!cat client/readonly_user_deployment.yaml\n", + "!kubectl apply -f \"client/readonly_user_deployment.yaml\"\n" + ], + "id": "14b7ad38368db767", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "apiVersion: apps/v1\r\n", + "kind: Deployment\r\n", + "metadata:\r\n", + " name: client-readonly-user\r\n", + " namespace: feast\r\n", + " labels:\r\n", + " app: client-user\r\n", + "spec:\r\n", + " replicas: 1\r\n", + " selector:\r\n", + " matchLabels:\r\n", + " app: client-user\r\n", + " template:\r\n", + " metadata:\r\n", + " labels:\r\n", + " app: client-user\r\n", + " spec:\r\n", + " serviceAccountName: feast-user-sa\r\n", + " containers:\r\n", + " - name: client-user-container\r\n", + " image: quay.io/feastdev/feature-server:latest\r\n", + " imagePullPolicy: Always\r\n", + " command: [\"sleep\", \"infinity\"]\r\n", + " volumeMounts:\r\n", + " - name: client-feature-repo-config\r\n", + " mountPath: /opt/app-root/src\r\n", + " volumes:\r\n", + " - name: client-feature-repo-config\r\n", + " configMap:\r\n", + " name: client-feature-repo-config\r\n", + "deployment.apps/client-readonly-user created\r\n" + ] + } + ], + "execution_count": 22 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "**Step 2: Run test.py script for client-readonly-user, readonly-user can only read or query all objects.**", + "id": "3b2f60e1fd32c0a5" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-14T15:03:43.446664Z", + "start_time": "2025-03-14T15:03:31.105949Z" + } + }, + "cell_type": "code", + "source": [ + "#Run test.py script from pod to test RBAC for client-readonly-user.\n", + "# verify the logs for write operation will show below message \n", + "# --- Write to Feature Store ---\n", + "#*** PERMISSION DENIED *** User lacks permission to modify the feature store.\n", + "\n", + "!kubectl exec -n feast -it $(kubectl get pods -n feast -l app=client-user -o jsonpath=\"{.items[0].metadata.name}\") -- python test.py\n" + ], + "id": "c33f1966259a8a18", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + ": MADV_DONTNEED does not work (memset will be used instead)\r\n", + ": (This is the expected behaviour if you are running under QEMU)\r\n", + "\r\n", + "--- List feature views ---\r\n", + "Successfully listed 2 feature views:\r\n", + " - driver_hourly_stats\r\n", + " - driver_hourly_stats_fresh\r\n", + "\r\n", + "--- Fetching Historical Features for Training ---\r\n", + "Successfully fetched training historical features:\r\n", + " driver_id ... conv_rate_plus_val2\r\n", + "0 1001 ... 10.229559\r\n", + "1 1002 ... 20.697800\r\n", + "2 1003 ... 30.933721\r\n", + "\r\n", + "[3 rows x 10 columns]\r\n", + "\r\n", + "--- Fetching Historical Features for Batch Scoring ---\r\n", + "Successfully fetched batch scoring historical features:\r\n", + " driver_id ... conv_rate_plus_val2\r\n", + "0 1002 ... 20.445888\r\n", + "1 1001 ... 10.815464\r\n", + "2 1003 ... 30.287972\r\n", + "\r\n", + "[3 rows x 10 columns]\r\n", + "\r\n", + "--- Write to Feature Store ---\r\n", + "\r\n", + "*** PERMISSION DENIED *** User lacks permission to modify the feature store.\r\n", + "\r\n", + "--- Fetching Online Features ---\r\n", + "Successfully fetched online features directly:\r\n", + "\r\n", + "acc_rate : [None, None]\r\n", + "conv_rate_plus_val1 : [None, None]\r\n", + "conv_rate_plus_val2 : [None, None]\r\n", + "driver_id : [1001, 1002]\r\n", + "\r\n", + "--- Fetching Online Features via Feature Service ---\r\n", + "Successfully fetched online features via feature service:\r\n", + "\r\n", + "conv_rate : [None, None]\r\n", + "conv_rate_plus_val1 : [None, None]\r\n", + "conv_rate_plus_val2 : [None, None]\r\n", + "driver_id : [1001, 1002]\r\n", + "\r\n", + "--- Fetching Online Features via Push Source ---\r\n", + "Successfully fetched online features via feature service:\r\n", + "\r\n", + "acc_rate : [None, None]\r\n", + "avg_daily_trips : [None, None]\r\n", + "conv_rate : [None, None]\r\n", + "conv_rate_plus_val1 : [None, None]\r\n", + "conv_rate_plus_val2 : [None, None]\r\n", + "driver_id : [1001, 1002]\r\n", + "\r\n", + "--- Performing Push Source ---\r\n", + "Unexpected error while pushing event: \r\n", + "Exception ignored in: \r\n", + "Traceback (most recent call last):\r\n", + " File \"/opt/app-root/lib64/python3.11/site-packages/feast/infra/registry/remote.py\", line 111, in __del__\r\n", + " File \"/opt/app-root/lib64/python3.11/site-packages/feast/infra/registry/remote.py\", line 108, in close\r\n", + " File \"/opt/app-root/lib64/python3.11/site-packages/grpc/_interceptor.py\", line 782, in close\r\n", + " File \"/opt/app-root/lib64/python3.11/site-packages/grpc/_channel.py\", line 2250, in close\r\n", + " File \"/opt/app-root/lib64/python3.11/site-packages/grpc/_channel.py\", line 2231, in _close\r\n", + "AttributeError: 'NoneType' object has no attribute 'StatusCode'\r\n" + ] + } + ], + "execution_count": 23 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "### Test Unauthorized Feast User ", + "id": "e5e63a172da6d6d7" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-14T15:05:21.599673Z", + "start_time": "2025-03-14T15:05:21.286300Z" + } + }, + "cell_type": "code", + "source": "!kubectl apply -f \"client/unauthorized_user_deployment.yaml\"", + "id": "7fb94439606b4077", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "deployment.apps/client-unauthorized-user created\r\n" + ] + } + ], + "execution_count": 24 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-14T15:05:36.709759Z", + "start_time": "2025-03-14T15:05:26.828228Z" + } + }, + "cell_type": "code", + "source": "!kubectl exec -n feast -it $(kubectl get pods -n feast -l app=client-unauthorized-user -o jsonpath=\"{.items[0].metadata.name}\") -- python test.py", + "id": "7aea5658325ab008", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + ": MADV_DONTNEED does not work (memset will be used instead)\r\n", + ": (This is the expected behaviour if you are running under QEMU)\r\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'.\r\n", + " DUMMY_ENTITY = Entity(\r\n", + "\r\n", + "--- List feature views ---\r\n", + "No feature views found. You might not have access or they haven't been created.\r\n", + "\r\n", + "--- Fetching Historical Features for Training ---\r\n", + "\r\n", + "*** PERMISSION DENIED *** Cannot fetch historical features.\r\n", + "\r\n", + "--- Fetching Historical Features for Batch Scoring ---\r\n", + "\r\n", + "*** PERMISSION DENIED *** Cannot fetch historical features.\r\n", + "\r\n", + "--- Write to Feature Store ---\r\n", + "\r\n", + "*** PERMISSION DENIED *** User lacks permission to modify the feature store.\r\n", + "\r\n", + "--- Fetching Online Features ---\r\n", + "\r\n", + "*** PERMISSION DENIED *** Cannot fetch online features.\r\n", + "\r\n", + "--- Fetching Online Features via Feature Service ---\r\n", + "\r\n", + "*** PERMISSION DENIED *** Cannot fetch online features.\r\n", + "\r\n", + "--- Fetching Online Features via Push Source ---\r\n", + "\r\n", + "*** PERMISSION DENIED *** Cannot fetch online features.\r\n", + "\r\n", + "--- Performing Push Source ---\r\n", + "Unexpected error while pushing event: Unable to find push source 'driver_stats_push_source'.\r\n", + "Exception ignored in: \r\n", + "Traceback (most recent call last):\r\n", + " File \"/opt/app-root/lib64/python3.11/site-packages/feast/infra/registry/remote.py\", line 111, in __del__\r\n", + " File \"/opt/app-root/lib64/python3.11/site-packages/feast/infra/registry/remote.py\", line 108, in close\r\n", + " File \"/opt/app-root/lib64/python3.11/site-packages/grpc/_interceptor.py\", line 782, in close\r\n", + " File \"/opt/app-root/lib64/python3.11/site-packages/grpc/_channel.py\", line 2250, in close\r\n", + " File \"/opt/app-root/lib64/python3.11/site-packages/grpc/_channel.py\", line 2231, in _close\r\n", + "AttributeError: 'NoneType' object has no attribute 'StatusCode'\r\n" + ] + } + ], + "execution_count": 25 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## Test Admin Feast User", + "id": "cb78ced7c37ceb4c" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-14T15:05:48.131836Z", + "start_time": "2025-03-14T15:05:47.921200Z" + } + }, + "cell_type": "code", + "source": "!kubectl apply -f \"client/admin_user_deployment.yaml\"", + "id": "2ee693d2436e282a", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "deployment.apps/client-admin-user created\r\n" + ] + } + ], + "execution_count": 26 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-14T15:06:04.283659Z", + "start_time": "2025-03-14T15:05:51.977649Z" + } + }, + "cell_type": "code", + "source": "!kubectl exec -n feast -it $(kubectl get pods -n feast -l app=client-admin -o jsonpath=\"{.items[0].metadata.name}\") -- python test.py\n", + "id": "7a6133f052b9cfe1", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + ": MADV_DONTNEED does not work (memset will be used instead)\r\n", + ": (This is the expected behaviour if you are running under QEMU)\r\n", + "\r\n", + "--- List feature views ---\r\n", + "Successfully listed 2 feature views:\r\n", + " - driver_hourly_stats\r\n", + " - driver_hourly_stats_fresh\r\n", + "\r\n", + "--- Fetching Historical Features for Training ---\r\n", + "Successfully fetched training historical features:\r\n", + " driver_id ... conv_rate_plus_val2\r\n", + "0 1001 ... 10.229559\r\n", + "1 1002 ... 20.697800\r\n", + "2 1003 ... 30.933721\r\n", + "\r\n", + "[3 rows x 10 columns]\r\n", + "\r\n", + "--- Fetching Historical Features for Batch Scoring ---\r\n", + "Successfully fetched batch scoring historical features:\r\n", + " driver_id ... conv_rate_plus_val2\r\n", + "0 1002 ... 20.445888\r\n", + "1 1001 ... 10.815464\r\n", + "2 1003 ... 30.287972\r\n", + "\r\n", + "[3 rows x 10 columns]\r\n", + "\r\n", + "--- Write to Feature Store ---\r\n", + "User has write access to the feature store.\r\n", + "\r\n", + "--- Fetching Online Features ---\r\n", + "Successfully fetched online features directly:\r\n", + "\r\n", + "acc_rate : [None, None]\r\n", + "conv_rate_plus_val1 : [None, None]\r\n", + "conv_rate_plus_val2 : [None, None]\r\n", + "driver_id : [1001, 1002]\r\n", + "\r\n", + "--- Fetching Online Features via Feature Service ---\r\n", + "Successfully fetched online features via feature service:\r\n", + "\r\n", + "conv_rate : [None, None]\r\n", + "conv_rate_plus_val1 : [None, None]\r\n", + "conv_rate_plus_val2 : [None, None]\r\n", + "driver_id : [1001, 1002]\r\n", + "\r\n", + "--- Fetching Online Features via Push Source ---\r\n", + "Successfully fetched online features via feature service:\r\n", + "\r\n", + "acc_rate : [None, None]\r\n", + "avg_daily_trips : [None, None]\r\n", + "conv_rate : [None, None]\r\n", + "conv_rate_plus_val1 : [None, None]\r\n", + "conv_rate_plus_val2 : [None, None]\r\n", + "driver_id : [1001, 1002]\r\n", + "\r\n", + "--- Performing Push Source ---\r\n", + "Unexpected error while pushing event: \r\n", + "Exception ignored in: \r\n", + "Traceback (most recent call last):\r\n", + " File \"/opt/app-root/lib64/python3.11/site-packages/feast/infra/registry/remote.py\", line 111, in __del__\r\n", + " File \"/opt/app-root/lib64/python3.11/site-packages/feast/infra/registry/remote.py\", line 108, in close\r\n", + " File \"/opt/app-root/lib64/python3.11/site-packages/grpc/_interceptor.py\", line 782, in close\r\n", + " File \"/opt/app-root/lib64/python3.11/site-packages/grpc/_channel.py\", line 2250, in close\r\n", + " File \"/opt/app-root/lib64/python3.11/site-packages/grpc/_channel.py\", line 2231, in _close\r\n", + "AttributeError: 'NoneType' object has no attribute 'StatusCode'\r\n" + ] + } + ], + "execution_count": 27 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "[Next: Client example on local](./3-client-rbac-test-local.ipynb)", + "id": "38c54e92643e0bda" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/operator-rbac/2-client.ipynb b/examples/operator-rbac/3-client-rbac-test-local.ipynb similarity index 56% rename from examples/operator-rbac/2-client.ipynb rename to examples/operator-rbac/3-client-rbac-test-local.ipynb index cf9d57cb5bc..c9ef68dd0f8 100644 --- a/examples/operator-rbac/2-client.ipynb +++ b/examples/operator-rbac/3-client-rbac-test-local.ipynb @@ -4,35 +4,27 @@ "metadata": {}, "cell_type": "markdown", "source": [ - "## Feast Client with RBAC\n", - "### Kubernetes RBAC Authorization\n", "\n", - "## Feast Role-Based Access Control (RBAC) in Kubernetes \n", + "# Feast Client with RBAC \n", + "### Example Using outside of Kubernetes for local testing\n", "\n", - "Feast **Role-Based Access Control (RBAC)** in Kubernetes supports authentication both **inside a Kubernetes pod** and for **external clients** using the `LOCAL_K8S_TOKEN` environment variable. \n", + "This notebook will test Feast authentication outside of Kubernetes for local testing.\n", "\n", + "When running outside of Kubernetes, you need to manually set the service account token in the `LOCAL_K8S_TOKEN` environment variable. The token can be retrieved from a running pod using: \n", + "\n", + "```sh\n", + "\n", + "kubectl exec -- cat /var/run/secrets/kubernetes.io/serviceaccount/token\n", "\n", - "### Inside a Kubernetes Pod\n", - "Feast automatically retrieves the Kubernetes ServiceAccount token from:\n", - "```\n", - "/var/run/secrets/kubernetes.io/serviceaccount/token\n", "```\n", - "This means:\n", - "- No manual configuration is needed inside a pod.\n", - "- The token is mounted automatically and used for authentication.\n", - "- Developer just need create the binding with role and service account accordingly.\n", - "- Code Reference: \n", - "[Feast Kubernetes Auth Client Manager (Pod Token Usage)](https://github.com/feast-dev/feast/blob/master/sdk/python/feast/permissions/client/kubernetes_auth_client_manager.py#L15) \n", - "- Using a service account from a pod [Example](https://github.com/feast-dev/feast/blob/master/examples/rbac-remote/client/k8s/)\n", "\n", - "### Outside a Kubernetes Pod (External Clients & Local Testing)\n", - " \n", - "If running Feast outside of Kubernetes, authentication requires setting the token manually to the environment variable `LOCAL_K8S_TOKEN` :\n", + "To authenticate Feast externally, set the retrieved token as an environment variable: \n", + "\n", "```sh\n", + "\n", "export LOCAL_K8S_TOKEN=\"your-service-account-token\"\n", - "```\n", "\n", - "For more details, refer the user guide: [Kubernetes RBAC Authorization](https://docs.feast.dev/master/getting-started/components/authz_manager#kubernetes-rbac-authorization) \n" + "``` \n" ], "id": "bb0145c9c1f6ebcc" }, @@ -58,12 +50,12 @@ { "metadata": { "ExecuteTime": { - "end_time": "2025-03-06T18:47:45.151296Z", - "start_time": "2025-03-06T18:47:45.024854Z" + "end_time": "2025-03-14T14:56:16.529917Z", + "start_time": "2025-03-14T14:56:16.361968Z" } }, "cell_type": "code", - "source": "!cat client/feature_store.yaml", + "source": "!cat client/feature_repo/feature_store.yaml", "id": "fac5f67ff391b5cf", "outputs": [ { @@ -88,19 +80,19 @@ ] } ], - "execution_count": 1 + "execution_count": 17 }, { "metadata": {}, "cell_type": "markdown", - "source": "**The Operator client feature store ConfigMap** containing the `feature_store.yaml `settings. We can retrieve it and port froward to local as we are testing locally.", + "source": "**The Operator create client feature store ConfigMap** containing the `feature_store.yaml `settings. We can retrieve it and port froward to local as we are testing locally.", "id": "84f73e09711bff9f" }, { "metadata": { "ExecuteTime": { - "end_time": "2025-03-06T18:46:36.029308Z", - "start_time": "2025-03-06T18:46:35.712532Z" + "end_time": "2025-03-14T14:56:42.630556Z", + "start_time": "2025-03-14T14:56:42.455312Z" } }, "cell_type": "code", @@ -129,7 +121,7 @@ ] } ], - "execution_count": 34 + "execution_count": 18 }, { "metadata": {}, @@ -146,8 +138,8 @@ { "metadata": { "ExecuteTime": { - "end_time": "2025-03-06T18:47:55.237205Z", - "start_time": "2025-03-06T18:47:55.226143Z" + "end_time": "2025-03-14T14:57:03.137990Z", + "start_time": "2025-03-14T14:57:03.096768Z" } }, "cell_type": "code", @@ -181,7 +173,7 @@ ] } ], - "execution_count": 2 + "execution_count": 19 }, { "metadata": {}, @@ -192,14 +184,13 @@ { "metadata": { "ExecuteTime": { - "end_time": "2025-03-06T18:48:00.150752Z", - "start_time": "2025-03-06T18:48:00.143370Z" + "end_time": "2025-03-14T14:57:42.336072Z", + "start_time": "2025-03-14T14:57:42.332818Z" } }, "cell_type": "code", "source": [ "import subprocess\n", - "import os\n", "\n", "def get_k8s_token(service_account):\n", " namespace = \"feast\"\n", @@ -222,7 +213,7 @@ ], "id": "70bdbcd7b3fe44", "outputs": [], - "execution_count": 3 + "execution_count": 20 }, { "metadata": {}, @@ -233,173 +224,204 @@ ], "id": "8c9e27ec4ed8ca2c" }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "", + "id": "856dcffcb8f19705" + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-14T14:58:09.731552Z", + "start_time": "2025-03-14T14:58:06.599552Z" + } + }, + "cell_type": "code", + "source": "!cat client/feature_repo/test.py", + "id": "934963c5f6b18930", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "import os\r\n", + "\r\n", + "from feast import FeatureStore\r\n", + "from feast.data_source import PushMode\r\n", + "from datetime import datetime\r\n", + "import pandas as pd\r\n", + "\r\n", + "# Initialize Feature Store\r\n", + "repo_path = os.getenv(\"FEAST_REPO_PATH\", \".\")\r\n", + "store = FeatureStore(repo_path=repo_path)\r\n", + "\r\n", + "def fetch_historical_features_entity_df(store: FeatureStore, for_batch_scoring: bool):\r\n", + " \"\"\"Fetch historical features for training or batch scoring.\"\"\"\r\n", + " try:\r\n", + " entity_df = pd.DataFrame.from_dict(\r\n", + " {\r\n", + " \"driver_id\": [1001, 1002, 1003],\r\n", + " \"event_timestamp\": [\r\n", + " datetime(2021, 4, 12, 10, 59, 42),\r\n", + " datetime(2021, 4, 12, 8, 12, 10),\r\n", + " datetime(2021, 4, 12, 16, 40, 26),\r\n", + " ],\r\n", + " \"label_driver_reported_satisfaction\": [1, 5, 3],\r\n", + " \"val_to_add\": [1, 2, 3],\r\n", + " \"val_to_add_2\": [10, 20, 30],\r\n", + " }\r\n", + " )\r\n", + " if for_batch_scoring:\r\n", + " entity_df[\"event_timestamp\"] = pd.to_datetime(\"now\", utc=True)\r\n", + "\r\n", + " training_df = store.get_historical_features(\r\n", + " entity_df=entity_df,\r\n", + " features=[\r\n", + " \"driver_hourly_stats:conv_rate\",\r\n", + " \"driver_hourly_stats:acc_rate\",\r\n", + " \"driver_hourly_stats:avg_daily_trips\",\r\n", + " \"transformed_conv_rate:conv_rate_plus_val1\",\r\n", + " \"transformed_conv_rate:conv_rate_plus_val2\",\r\n", + " ],\r\n", + " ).to_df()\r\n", + " print(f\"Successfully fetched {'batch scoring' if for_batch_scoring else 'training'} historical features:\\n\", training_df.head())\r\n", + "\r\n", + " except PermissionError:\r\n", + " print(\"\\n*** PERMISSION DENIED *** Cannot fetch historical features.\")\r\n", + " except Exception as e:\r\n", + " print(f\"Unexpected error while fetching historical features: {e}\")\r\n", + "\r\n", + "def fetch_online_features(store: FeatureStore, source: str = \"\"):\r\n", + " \"\"\"Fetch online features from the feature store.\"\"\"\r\n", + " try:\r\n", + " entity_rows = [\r\n", + " {\r\n", + " \"driver_id\": 1001,\r\n", + " \"val_to_add\": 1000,\r\n", + " \"val_to_add_2\": 2000,\r\n", + " },\r\n", + " {\r\n", + " \"driver_id\": 1002,\r\n", + " \"val_to_add\": 1001,\r\n", + " \"val_to_add_2\": 2002,\r\n", + " },\r\n", + " ]\r\n", + " if source == \"feature_service\":\r\n", + " features_to_fetch = store.get_feature_service(\"driver_activity_v1\")\r\n", + " elif source == \"push\":\r\n", + " features_to_fetch = store.get_feature_service(\"driver_activity_v3\")\r\n", + " else:\r\n", + " features_to_fetch = [\r\n", + " \"driver_hourly_stats:acc_rate\",\r\n", + " \"transformed_conv_rate:conv_rate_plus_val1\",\r\n", + " \"transformed_conv_rate:conv_rate_plus_val2\",\r\n", + " ]\r\n", + "\r\n", + " returned_features = store.get_online_features(\r\n", + " features=features_to_fetch,\r\n", + " entity_rows=entity_rows,\r\n", + " ).to_dict()\r\n", + "\r\n", + " print(f\"Successfully fetched online features {'via feature service' if source else 'directly'}:\\n\")\r\n", + " for key, value in sorted(returned_features.items()):\r\n", + " print(f\"{key} : {value}\")\r\n", + "\r\n", + " except PermissionError:\r\n", + " print(\"\\n*** PERMISSION DENIED *** Cannot fetch online features.\")\r\n", + " except Exception as e:\r\n", + " print(f\"Unexpected error while fetching online features: {e}\")\r\n", + "\r\n", + "def check_permissions():\r\n", + " \"\"\"Check user role, test various Feast operations.\"\"\"\r\n", + " feature_views = []\r\n", + "\r\n", + " # Step 1: List feature views\r\n", + " print(\"\\n--- List feature views ---\")\r\n", + " try:\r\n", + " feature_views = store.list_feature_views()\r\n", + " if not feature_views:\r\n", + " print(\"No feature views found. You might not have access or they haven't been created.\")\r\n", + " else:\r\n", + " print(f\"Successfully listed {len(feature_views)} feature views:\")\r\n", + " for fv in feature_views:\r\n", + " print(f\" - {fv.name}\")\r\n", + " except PermissionError:\r\n", + " print(\"\\n*** PERMISSION DENIED *** Cannot list feature views.\")\r\n", + " except Exception as e:\r\n", + " print(f\"Unexpected error listing feature views: {e}\")\r\n", + "\r\n", + " # Step 2: Fetch Historical Features\r\n", + " print(\"\\n--- Fetching Historical Features for Training ---\")\r\n", + " fetch_historical_features_entity_df(store, for_batch_scoring=False)\r\n", + "\r\n", + " print(\"\\n--- Fetching Historical Features for Batch Scoring ---\")\r\n", + " fetch_historical_features_entity_df(store, for_batch_scoring=True)\r\n", + "\r\n", + " # Step 3: Apply Feature Store\r\n", + " print(\"\\n--- Write to Feature Store ---\")\r\n", + " try:\r\n", + " store.apply(feature_views)\r\n", + " print(\"User has write access to the feature store.\")\r\n", + " except PermissionError:\r\n", + " print(\"\\n*** PERMISSION DENIED *** User lacks permission to modify the feature store.\")\r\n", + " except Exception as e:\r\n", + " print(f\"Unexpected error testing write access: {e}\")\r\n", + "\r\n", + " # Step 4: Fetch Online Features\r\n", + " print(\"\\n--- Fetching Online Features ---\")\r\n", + " fetch_online_features(store)\r\n", + "\r\n", + " print(\"\\n--- Fetching Online Features via Feature Service ---\")\r\n", + " fetch_online_features(store, source=\"feature_service\")\r\n", + "\r\n", + " print(\"\\n--- Fetching Online Features via Push Source ---\")\r\n", + " fetch_online_features(store, source=\"push\")\r\n", + "\r\n", + " print(\"\\n--- Performing Push Source ---\")\r\n", + " # Step 5: Simulate Event Push (Streaming Ingestion)\r\n", + " try:\r\n", + " event_df = pd.DataFrame.from_dict(\r\n", + " {\r\n", + " \"driver_id\": [1001],\r\n", + " \"event_timestamp\": [datetime.now()],\r\n", + " \"created\": [datetime.now()],\r\n", + " \"conv_rate\": [1.0],\r\n", + " \"acc_rate\": [1.0],\r\n", + " \"avg_daily_trips\": [1000],\r\n", + " }\r\n", + " )\r\n", + " store.push(\"driver_stats_push_source\", event_df, to=PushMode.ONLINE_AND_OFFLINE)\r\n", + " print(\"Successfully pushed a test event.\")\r\n", + " except PermissionError:\r\n", + " print(\"\\n*** PERMISSION DENIED *** Cannot push event (no write access).\")\r\n", + " except Exception as e:\r\n", + " print(f\"Unexpected error while pushing event: {e}\")\r\n", + "\r\n", + "if __name__ == \"__main__\":\r\n", + " check_permissions()\r\n" + ] + } + ], + "execution_count": 21 + }, { "metadata": { "ExecuteTime": { - "end_time": "2025-03-06T20:16:04.254201Z", - "start_time": "2025-03-06T20:16:04.245605Z" + "end_time": "2025-03-14T14:58:15.658631Z", + "start_time": "2025-03-14T14:58:15.653740Z" } }, "cell_type": "code", "source": [ - "from feast import FeatureStore\n", - "from feast.data_source import PushMode\n", - "from datetime import datetime\n", - "import pandas as pd\n", - "\n", - "# Initialize Feature Store\n", - "store = FeatureStore(repo_path=\"client\")\n", - "\n", - "def fetch_historical_features_entity_df(store: FeatureStore, for_batch_scoring: bool):\n", - " \"\"\"Fetch historical features for training or batch scoring.\"\"\"\n", - " try:\n", - " entity_df = pd.DataFrame.from_dict(\n", - " {\n", - " \"driver_id\": [1001, 1002, 1003],\n", - " \"event_timestamp\": [\n", - " datetime(2021, 4, 12, 10, 59, 42),\n", - " datetime(2021, 4, 12, 8, 12, 10),\n", - " datetime(2021, 4, 12, 16, 40, 26),\n", - " ],\n", - " \"label_driver_reported_satisfaction\": [1, 5, 3],\n", - " \"val_to_add\": [1, 2, 3],\n", - " \"val_to_add_2\": [10, 20, 30],\n", - " }\n", - " )\n", - " if for_batch_scoring:\n", - " entity_df[\"event_timestamp\"] = pd.to_datetime(\"now\", utc=True)\n", - "\n", - " training_df = store.get_historical_features(\n", - " entity_df=entity_df,\n", - " features=[\n", - " \"driver_hourly_stats:conv_rate\",\n", - " \"driver_hourly_stats:acc_rate\",\n", - " \"driver_hourly_stats:avg_daily_trips\",\n", - " \"transformed_conv_rate:conv_rate_plus_val1\",\n", - " \"transformed_conv_rate:conv_rate_plus_val2\",\n", - " ],\n", - " ).to_df()\n", - " print(f\"Successfully fetched {'batch scoring' if for_batch_scoring else 'training'} historical features:\\n\", training_df.head())\n", - "\n", - " except PermissionError:\n", - " print(\"\\n*** PERMISSION DENIED *** Cannot fetch historical features.\")\n", - " except Exception as e:\n", - " print(f\"Unexpected error while fetching historical features: {e}\")\n", - "\n", - "def fetch_online_features(store: FeatureStore, source: str = \"\"):\n", - " \"\"\"Fetch online features from the feature store.\"\"\"\n", - " try:\n", - " entity_rows = [\n", - " {\n", - " \"driver_id\": 1001,\n", - " \"val_to_add\": 1000,\n", - " \"val_to_add_2\": 2000,\n", - " },\n", - " {\n", - " \"driver_id\": 1002,\n", - " \"val_to_add\": 1001,\n", - " \"val_to_add_2\": 2002,\n", - " },\n", - " ]\n", - " if source == \"feature_service\":\n", - " features_to_fetch = store.get_feature_service(\"driver_activity_v1\")\n", - " elif source == \"push\":\n", - " features_to_fetch = store.get_feature_service(\"driver_activity_v3\")\n", - " else:\n", - " features_to_fetch = [\n", - " \"driver_hourly_stats:acc_rate\",\n", - " \"transformed_conv_rate:conv_rate_plus_val1\",\n", - " \"transformed_conv_rate:conv_rate_plus_val2\",\n", - " ]\n", - "\n", - " returned_features = store.get_online_features(\n", - " features=features_to_fetch,\n", - " entity_rows=entity_rows,\n", - " ).to_dict()\n", - "\n", - " print(f\"Successfully fetched online features {'via feature service' if source else 'directly'}:\\n\")\n", - " for key, value in sorted(returned_features.items()):\n", - " print(f\"{key} : {value}\")\n", - "\n", - " except PermissionError:\n", - " print(\"\\n*** PERMISSION DENIED *** Cannot fetch online features.\")\n", - " except Exception as e:\n", - " print(f\"Unexpected error while fetching online features: {e}\")\n", - "\n", - "def check_permissions():\n", - " \"\"\"Check user role, test various Feast operations,.\"\"\"\n", - "\n", - " feature_views = []\n", - "\n", - " # Step 1: List feature views\n", - " print(\"\\n--- List feature views ---\")\n", - " try:\n", - " feature_views = store.list_feature_views()\n", - " if not feature_views:\n", - " print(\"No feature views found. You might not have access or they haven't been created.\")\n", - " has_feature_view_access = False\n", - " else:\n", - " print(f\"Successfully listed {len(feature_views)} feature views:\")\n", - " for fv in feature_views:\n", - " print(f\" - {fv.name}\")\n", - "\n", - " except PermissionError:\n", - " print(\"\\n*** PERMISSION DENIED *** Cannot list feature views.\")\n", - " has_feature_view_access = False\n", - " except Exception as e:\n", - " print(f\"Unexpected error listing feature views: {e}\")\n", - " has_feature_view_access = False\n", - "\n", - " # Step 2: Fetch Historical Features\n", - " print(\"\\n--- Fetching Historical Features for Training ---\")\n", - " fetch_historical_features_entity_df(store, for_batch_scoring=False)\n", - "\n", - " print(\"\\n--- Fetching Historical Features for Batch Scoring ---\")\n", - " fetch_historical_features_entity_df(store, for_batch_scoring=True)\n", - "\n", - " # Step 3: Apply Feature Store\n", - " print(\"\\n--- Write to Feature Store ---\")\n", - " try:\n", - " store.apply(feature_views)\n", - " print(\"User has write access to the feature store.\")\n", - " except PermissionError:\n", - " print(\"\\n*** PERMISSION DENIED *** User lacks permission to modify the feature store.\")\n", - " except Exception as e:\n", - " print(f\"Unexpected error testing write access: {e}\")\n", - "\n", - " # Step 4: Fetch Online Features\n", - " print(\"\\n--- Fetching Online Features ---\")\n", - " fetch_online_features(store)\n", - "\n", - " print(\"\\n--- Fetching Online Features via Feature Service ---\")\n", - " fetch_online_features(store, source=\"feature_service\")\n", - "\n", - " print(\"\\n--- Fetching Online Features via Push Source ---\")\n", - " fetch_online_features(store, source=\"push\")\n", + "import os\n", "\n", - " print(\"\\n--- Performing Push Source ---\")\n", - " # Step 5: Simulate Event Push (Streaming Ingestion)\n", - " try:\n", - " event_df = pd.DataFrame.from_dict(\n", - " {\n", - " \"driver_id\": [1001],\n", - " \"event_timestamp\": [datetime.now()],\n", - " \"created\": [datetime.now()],\n", - " \"conv_rate\": [1.0],\n", - " \"acc_rate\": [1.0],\n", - " \"avg_daily_trips\": [1000],\n", - " }\n", - " )\n", - " store.push(\"driver_stats_push_source\", event_df, to=PushMode.ONLINE_AND_OFFLINE)\n", - " print(\"Successfully pushed a test event.\")\n", - " except PermissionError:\n", - " print(\"\\n*** PERMISSION DENIED *** Cannot push event (no write access).\")\n", - " except Exception as e:\n", - " print(f\"Unexpected error while pushing event: {e}\")\n" + "# Set the FEAST_REPO_PATH before importing check_permissions\n", + "os.environ[\"FEAST_REPO_PATH\"] = \"client/feature_repo\"" ], - "id": "934963c5f6b18930", + "id": "7c20ea331dc5a09b", "outputs": [], - "execution_count": 51 + "execution_count": 22 }, { "metadata": {}, @@ -413,8 +435,8 @@ { "metadata": { "ExecuteTime": { - "end_time": "2025-03-06T20:12:44.771268Z", - "start_time": "2025-03-06T20:12:44.691353Z" + "end_time": "2025-03-14T14:58:27.147260Z", + "start_time": "2025-03-14T14:58:24.035744Z" } }, "cell_type": "code", @@ -427,29 +449,36 @@ "'Token Retrieved: ***** (hidden for security)'" ] }, - "execution_count": 48, + "execution_count": 23, "metadata": {}, "output_type": "execute_result" } ], - "execution_count": 48 + "execution_count": 23 }, { "metadata": {}, "cell_type": "markdown", - "source": "**Step 2: Test misc functions from offline, online, materialize_incremental, and others**", + "source": "**Step 2: Test permission functions to validate permission on fetching online, offline or perform write operation**", "id": "140c909fa8bcc6ab" }, { "metadata": { "ExecuteTime": { - "end_time": "2025-03-06T20:16:16.680582Z", - "start_time": "2025-03-06T20:16:14.930480Z" + "end_time": "2025-03-14T14:58:57.328137Z", + "start_time": "2025-03-14T14:58:54.623074Z" } }, "cell_type": "code", "source": [ - "# Run the permission check function\n", + "from client.feature_repo.test import check_permissions\n", + "\n", + "# Call the function\n", + "#Run test.py script from pod to test RBAC for client-readonly-user.\n", + "# verify the logs for write operation will show below message \n", + "# --- Write to Feature Store ---\n", + "#*** PERMISSION DENIED *** User lacks permission to modify the feature store.\n", + "\n", "check_permissions()\n" ], "id": "14b7ad38368db767", @@ -460,6 +489,7 @@ "text": [ "\n", "--- List feature views ---\n", + "Handling connection for 8083\n", "Successfully listed 2 feature views:\n", " - driver_hourly_stats\n", " - driver_hourly_stats_fresh\n", @@ -473,32 +503,32 @@ "2 1003 2021-04-12 16:40:26+00:00 3 \n", "\n", " val_to_add val_to_add_2 conv_rate acc_rate avg_daily_trips \\\n", - "0 1 10 0.677818 0.453707 193 \n", - "1 2 20 0.328160 0.900565 929 \n", - "2 3 30 0.787191 0.958963 571 \n", + "0 1 10 0.229559 0.502340 613 \n", + "1 2 20 0.697800 0.872357 891 \n", + "2 3 30 0.933721 0.885730 781 \n", "\n", " conv_rate_plus_val1 conv_rate_plus_val2 \n", - "0 1.677818 10.677818 \n", - "1 2.328160 20.328160 \n", - "2 3.787191 30.787191 \n", + "0 1.229559 10.229559 \n", + "1 2.697800 20.697800 \n", + "2 3.933721 30.933721 \n", "\n", "--- Fetching Historical Features for Batch Scoring ---\n", "Handling connection for 8081\n", "Successfully fetched batch scoring historical features:\n", " driver_id event_timestamp \\\n", - "0 1001 2025-03-06 20:16:15.556223+00:00 \n", - "1 1002 2025-03-06 20:16:15.556223+00:00 \n", - "2 1003 2025-03-06 20:16:15.556223+00:00 \n", + "0 1001 2025-03-14 14:58:55.852884+00:00 \n", + "1 1002 2025-03-14 14:58:55.852884+00:00 \n", + "2 1003 2025-03-14 14:58:55.852884+00:00 \n", "\n", " label_driver_reported_satisfaction val_to_add val_to_add_2 conv_rate \\\n", - "0 1 1 10 0.782836 \n", - "1 5 2 20 0.731948 \n", - "2 3 3 30 0.613211 \n", + "0 1 1 10 0.815464 \n", + "1 5 2 20 0.445888 \n", + "2 3 3 30 0.287972 \n", "\n", " acc_rate avg_daily_trips conv_rate_plus_val1 conv_rate_plus_val2 \n", - "0 0.729726 652 1.782836 10.782836 \n", - "1 0.384902 902 2.731948 20.731948 \n", - "2 0.075386 101 3.613211 30.613211 \n", + "0 0.251485 465 1.815464 10.815464 \n", + "1 0.566106 646 2.445888 20.445888 \n", + "2 0.948905 916 3.287972 30.287972 \n", "\n", "--- Write to Feature Store ---\n", "\n", @@ -538,7 +568,7 @@ ] } ], - "execution_count": 53 + "execution_count": 24 }, { "metadata": {}, @@ -549,8 +579,8 @@ { "metadata": { "ExecuteTime": { - "end_time": "2025-03-06T20:16:38.487573Z", - "start_time": "2025-03-06T20:16:38.351889Z" + "end_time": "2025-03-14T14:59:12.042904Z", + "start_time": "2025-03-14T14:59:11.959795Z" } }, "cell_type": "code", @@ -566,18 +596,18 @@ "'Token Retrieved: ***** (hidden for security)'" ] }, - "execution_count": 54, + "execution_count": 25, "metadata": {}, "output_type": "execute_result" } ], - "execution_count": 54 + "execution_count": 25 }, { "metadata": { "ExecuteTime": { - "end_time": "2025-03-06T20:16:41.522132Z", - "start_time": "2025-03-06T20:16:41.254668Z" + "end_time": "2025-03-14T14:59:14.629414Z", + "start_time": "2025-03-14T14:59:14.375404Z" } }, "cell_type": "code", @@ -621,7 +651,7 @@ ] } ], - "execution_count": 55 + "execution_count": 26 }, { "metadata": {}, @@ -632,8 +662,8 @@ { "metadata": { "ExecuteTime": { - "end_time": "2025-03-06T20:17:02.206503Z", - "start_time": "2025-03-06T20:17:02.137409Z" + "end_time": "2025-03-14T14:59:23.745351Z", + "start_time": "2025-03-14T14:59:23.647728Z" } }, "cell_type": "code", @@ -649,18 +679,18 @@ "'Token Retrieved: ***** (hidden for security)'" ] }, - "execution_count": 56, + "execution_count": 27, "metadata": {}, "output_type": "execute_result" } ], - "execution_count": 56 + "execution_count": 27 }, { "metadata": { "ExecuteTime": { - "end_time": "2025-03-06T20:17:07.799782Z", - "start_time": "2025-03-06T20:17:05.946696Z" + "end_time": "2025-03-14T14:59:28.583546Z", + "start_time": "2025-03-14T14:59:26.745747Z" } }, "cell_type": "code", @@ -686,32 +716,32 @@ "2 1003 2021-04-12 16:40:26+00:00 3 \n", "\n", " val_to_add val_to_add_2 conv_rate acc_rate avg_daily_trips \\\n", - "0 1 10 0.677818 0.453707 193 \n", - "1 2 20 0.328160 0.900565 929 \n", - "2 3 30 0.787191 0.958963 571 \n", + "0 1 10 0.229559 0.502340 613 \n", + "1 2 20 0.697800 0.872357 891 \n", + "2 3 30 0.933721 0.885730 781 \n", "\n", " conv_rate_plus_val1 conv_rate_plus_val2 \n", - "0 1.677818 10.677818 \n", - "1 2.328160 20.328160 \n", - "2 3.787191 30.787191 \n", + "0 1.229559 10.229559 \n", + "1 2.697800 20.697800 \n", + "2 3.933721 30.933721 \n", "\n", "--- Fetching Historical Features for Batch Scoring ---\n", "Handling connection for 8081\n", "Successfully fetched batch scoring historical features:\n", " driver_id event_timestamp \\\n", - "0 1001 2025-03-06 20:17:06.566035+00:00 \n", - "1 1002 2025-03-06 20:17:06.566035+00:00 \n", - "2 1003 2025-03-06 20:17:06.566035+00:00 \n", + "0 1001 2025-03-14 14:59:27.350638+00:00 \n", + "1 1002 2025-03-14 14:59:27.350638+00:00 \n", + "2 1003 2025-03-14 14:59:27.350638+00:00 \n", "\n", " label_driver_reported_satisfaction val_to_add val_to_add_2 conv_rate \\\n", - "0 1 1 10 0.782836 \n", - "1 5 2 20 0.731948 \n", - "2 3 3 30 0.613211 \n", + "0 1 1 10 0.815464 \n", + "1 5 2 20 0.445888 \n", + "2 3 3 30 0.287972 \n", "\n", " acc_rate avg_daily_trips conv_rate_plus_val1 conv_rate_plus_val2 \n", - "0 0.729726 652 1.782836 10.782836 \n", - "1 0.384902 902 2.731948 20.731948 \n", - "2 0.075386 101 3.613211 30.613211 \n", + "0 0.251485 465 1.815464 10.815464 \n", + "1 0.566106 646 2.445888 20.445888 \n", + "2 0.948905 916 3.287972 30.287972 \n", "\n", "--- Write to Feature Store ---\n", "User has write access to the feature store.\n", @@ -750,7 +780,7 @@ ] } ], - "execution_count": 57 + "execution_count": 28 }, { "metadata": {}, @@ -773,8 +803,8 @@ { "metadata": { "ExecuteTime": { - "end_time": "2025-03-05T19:09:29.743583Z", - "start_time": "2025-03-05T19:09:29.734671Z" + "end_time": "2025-03-14T14:59:48.327362Z", + "start_time": "2025-03-14T14:59:48.324482Z" } }, "cell_type": "code", @@ -795,12 +825,12 @@ ] } ], - "execution_count": 25 + "execution_count": 29 }, { "metadata": {}, "cell_type": "markdown", - "source": "[Next: Uninstall the Operator and all Feast objects](./03-uninstall.ipynb)", + "source": "[Next: Uninstall the Operator and all Feast objects](./04-uninstall.ipynb)", "id": "38c54e92643e0bda" } ], diff --git a/examples/operator-rbac/README.md b/examples/operator-rbac/README.md index 9c0a0461678..73a51128bf5 100644 --- a/examples/operator-rbac/README.md +++ b/examples/operator-rbac/README.md @@ -1,6 +1,7 @@ # Running the Feast RBAC example on Kubernetes using the Feast Operator. 1. [1-setup-operator-rbac.ipynb](1-setup-operator-rbac.ipynb) will guide you through how to setup Role-Based Access Control (RBAC) for Feast using the [Feast Operator](../../infra/feast-operator/) and Kubernetes Authentication. This Feast Admin Step requires you to setup the operator and Feast RBAC on K8s. -2. [2-client.ipynb](2-client.ipynb) Validate the RBAC with the client example using different test cases using a service account token locally. -3. [03-uninstall.ipynb](03-uninstall.ipynb) Clear the installed deployments and K8s Objects. +2. [2-client-rbac-test-pod.ipynb](2-client-rbac-test-pod.ipynb) notebook from within a Kubernetes pod to validate RBAC. +3. [3-client-rbac-test-local.ipynb](3-client-rbac-test-local.ipynb) Validate the RBAC with the client example using different test cases using a service account token locally. +4. [04-uninstall.ipynb](04-uninstall.ipynb) Clear the installed deployments and K8s Objects. diff --git a/examples/operator-rbac/client/admin_user_deployment.yaml b/examples/operator-rbac/client/admin_user_deployment.yaml new file mode 100644 index 00000000000..c755ef5dad5 --- /dev/null +++ b/examples/operator-rbac/client/admin_user_deployment.yaml @@ -0,0 +1,30 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: client-admin-user + namespace: feast + labels: + app: client-admin +spec: + replicas: 1 + selector: + matchLabels: + app: client-admin + template: + metadata: + labels: + app: client-admin + spec: + serviceAccountName: feast-admin-sa + containers: + - name: client-admin-container + image: quay.io/feastdev/feature-server:latest + imagePullPolicy: Always + command: ["sleep", "infinity"] + volumeMounts: + - name: client-feature-repo-config + mountPath: /opt/app-root/src + volumes: + - name: client-feature-repo-config + configMap: + name: client-feature-repo-config diff --git a/examples/operator-rbac/client/feature_store.yaml b/examples/operator-rbac/client/feature_repo/feature_store.yaml similarity index 100% rename from examples/operator-rbac/client/feature_store.yaml rename to examples/operator-rbac/client/feature_repo/feature_store.yaml diff --git a/examples/operator-rbac/client/feature_repo/test.py b/examples/operator-rbac/client/feature_repo/test.py new file mode 100644 index 00000000000..78732327a62 --- /dev/null +++ b/examples/operator-rbac/client/feature_repo/test.py @@ -0,0 +1,155 @@ +import os + +from feast import FeatureStore +from feast.data_source import PushMode +from datetime import datetime +import pandas as pd + +# Initialize Feature Store +repo_path = os.getenv("FEAST_REPO_PATH", ".") +store = FeatureStore(repo_path=repo_path) + +def fetch_historical_features_entity_df(store: FeatureStore, for_batch_scoring: bool): + """Fetch historical features for training or batch scoring.""" + try: + entity_df = pd.DataFrame.from_dict( + { + "driver_id": [1001, 1002, 1003], + "event_timestamp": [ + datetime(2021, 4, 12, 10, 59, 42), + datetime(2021, 4, 12, 8, 12, 10), + datetime(2021, 4, 12, 16, 40, 26), + ], + "label_driver_reported_satisfaction": [1, 5, 3], + "val_to_add": [1, 2, 3], + "val_to_add_2": [10, 20, 30], + } + ) + if for_batch_scoring: + entity_df["event_timestamp"] = pd.to_datetime("now", utc=True) + + training_df = store.get_historical_features( + entity_df=entity_df, + features=[ + "driver_hourly_stats:conv_rate", + "driver_hourly_stats:acc_rate", + "driver_hourly_stats:avg_daily_trips", + "transformed_conv_rate:conv_rate_plus_val1", + "transformed_conv_rate:conv_rate_plus_val2", + ], + ).to_df() + print(f"Successfully fetched {'batch scoring' if for_batch_scoring else 'training'} historical features:\n", training_df.head()) + + except PermissionError: + print("\n*** PERMISSION DENIED *** Cannot fetch historical features.") + except Exception as e: + print(f"Unexpected error while fetching historical features: {e}") + +def fetch_online_features(store: FeatureStore, source: str = ""): + """Fetch online features from the feature store.""" + try: + entity_rows = [ + { + "driver_id": 1001, + "val_to_add": 1000, + "val_to_add_2": 2000, + }, + { + "driver_id": 1002, + "val_to_add": 1001, + "val_to_add_2": 2002, + }, + ] + if source == "feature_service": + features_to_fetch = store.get_feature_service("driver_activity_v1") + elif source == "push": + features_to_fetch = store.get_feature_service("driver_activity_v3") + else: + features_to_fetch = [ + "driver_hourly_stats:acc_rate", + "transformed_conv_rate:conv_rate_plus_val1", + "transformed_conv_rate:conv_rate_plus_val2", + ] + + returned_features = store.get_online_features( + features=features_to_fetch, + entity_rows=entity_rows, + ).to_dict() + + print(f"Successfully fetched online features {'via feature service' if source else 'directly'}:\n") + for key, value in sorted(returned_features.items()): + print(f"{key} : {value}") + + except PermissionError: + print("\n*** PERMISSION DENIED *** Cannot fetch online features.") + except Exception as e: + print(f"Unexpected error while fetching online features: {e}") + +def check_permissions(): + """Check user role, test various Feast operations.""" + feature_views = [] + + # Step 1: List feature views + print("\n--- List feature views ---") + try: + feature_views = store.list_feature_views() + if not feature_views: + print("No feature views found. You might not have access or they haven't been created.") + else: + print(f"Successfully listed {len(feature_views)} feature views:") + for fv in feature_views: + print(f" - {fv.name}") + except PermissionError: + print("\n*** PERMISSION DENIED *** Cannot list feature views.") + except Exception as e: + print(f"Unexpected error listing feature views: {e}") + + # Step 2: Fetch Historical Features + print("\n--- Fetching Historical Features for Training ---") + fetch_historical_features_entity_df(store, for_batch_scoring=False) + + print("\n--- Fetching Historical Features for Batch Scoring ---") + fetch_historical_features_entity_df(store, for_batch_scoring=True) + + # Step 3: Apply Feature Store + print("\n--- Write to Feature Store ---") + try: + store.apply(feature_views) + print("User has write access to the feature store.") + except PermissionError: + print("\n*** PERMISSION DENIED *** User lacks permission to modify the feature store.") + except Exception as e: + print(f"Unexpected error testing write access: {e}") + + # Step 4: Fetch Online Features + print("\n--- Fetching Online Features ---") + fetch_online_features(store) + + print("\n--- Fetching Online Features via Feature Service ---") + fetch_online_features(store, source="feature_service") + + print("\n--- Fetching Online Features via Push Source ---") + fetch_online_features(store, source="push") + + print("\n--- Performing Push Source ---") + # Step 5: Simulate Event Push (Streaming Ingestion) + try: + event_df = pd.DataFrame.from_dict( + { + "driver_id": [1001], + "event_timestamp": [datetime.now()], + "created": [datetime.now()], + "conv_rate": [1.0], + "acc_rate": [1.0], + "avg_daily_trips": [1000], + } + ) + store.push("driver_stats_push_source", event_df, to=PushMode.ONLINE_AND_OFFLINE) + print("Successfully pushed a test event.") + except PermissionError: + print("\n*** PERMISSION DENIED *** Cannot push event (no write access).") + except Exception as e: + print(f"Unexpected error while pushing event: {e}") + +if __name__ == "__main__": + check_permissions() diff --git a/examples/operator-rbac/client/readonly_user_deployment.yaml b/examples/operator-rbac/client/readonly_user_deployment.yaml new file mode 100644 index 00000000000..5146cff70c6 --- /dev/null +++ b/examples/operator-rbac/client/readonly_user_deployment.yaml @@ -0,0 +1,30 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: client-readonly-user + namespace: feast + labels: + app: client-user +spec: + replicas: 1 + selector: + matchLabels: + app: client-user + template: + metadata: + labels: + app: client-user + spec: + serviceAccountName: feast-user-sa + containers: + - name: client-user-container + image: quay.io/feastdev/feature-server:latest + imagePullPolicy: Always + command: ["sleep", "infinity"] + volumeMounts: + - name: client-feature-repo-config + mountPath: /opt/app-root/src + volumes: + - name: client-feature-repo-config + configMap: + name: client-feature-repo-config diff --git a/examples/operator-rbac/client/unauthorized_user_deployment.yaml b/examples/operator-rbac/client/unauthorized_user_deployment.yaml new file mode 100644 index 00000000000..6ef2ae0e462 --- /dev/null +++ b/examples/operator-rbac/client/unauthorized_user_deployment.yaml @@ -0,0 +1,30 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: client-unauthorized-user + namespace: feast + labels: + app: client-unauthorized-user +spec: + replicas: 1 + selector: + matchLabels: + app: client-unauthorized-user + template: + metadata: + labels: + app: client-unauthorized-user + spec: + serviceAccountName: feast-unauthorized-user-sa + containers: + - name: client-unauthorized-user-container + image: feastdev/feature-server:latest + imagePullPolicy: Always + command: ["sleep", "infinity"] + volumeMounts: + - name: client-feature-repo-config + mountPath: /opt/app-root/src + volumes: + - name: client-feature-repo-config + configMap: + name: client-feature-repo-config