Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ package e2erhoai

import (
"fmt"
"time"

utils "github.com/feast-dev/feast/infra/feast-operator/test/utils"
. "github.com/onsi/ginkgo/v2"
Expand All @@ -43,6 +44,60 @@ var _ = Describe("Feast Workbench Integration Connection Testing", Ordered, func
feastCRName = "credit-scoring"
)

// Verify feast ConfigMap
verifyFeastConfigMap := func(authEnabled bool) {
feastConfigMapName := "jupyter-nb-kube-3aadmin-feast-config"
configMapKey := "credit_scoring_local"
By(fmt.Sprintf("Listing ConfigMaps and verifying %s exists with correct content", feastConfigMapName))

// Build expected content based on auth type
expectedContent := []string{
"project: credit_scoring_local",
}
if authEnabled {
expectedContent = append(expectedContent, "type: kubernetes")
} else {
expectedContent = append(expectedContent, "type: no_auth")
}

// First, list ConfigMaps and check if target ConfigMap exists
// Retry with polling since the ConfigMap may be created asynchronously
const maxRetries = 5
const retryInterval = 5 * time.Second
var configMapExists bool
var err error

for i := 0; i < maxRetries; i++ {
exists, listErr := utils.VerifyConfigMapExistsInList(namespace, feastConfigMapName)
if listErr != nil {
err = listErr
if i < maxRetries-1 {
fmt.Printf("Failed to list ConfigMaps, retrying in %v... (attempt %d/%d)\n", retryInterval, i+1, maxRetries)
time.Sleep(retryInterval)
continue
}
} else if exists {
configMapExists = true
fmt.Printf("ConfigMap %s found in ConfigMap list\n", feastConfigMapName)
break
}

if i < maxRetries-1 {
fmt.Printf("ConfigMap %s not found in list yet, retrying in %v... (attempt %d/%d)\n", feastConfigMapName, retryInterval, i+1, maxRetries)
time.Sleep(retryInterval)
}
}

if !configMapExists {
Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("Failed to find ConfigMap %s in ConfigMap list after %d attempts: %v", feastConfigMapName, maxRetries, err))
}

// Once ConfigMap exists in list, verify content (project name and auth type)
err = utils.VerifyFeastConfigMapContent(namespace, feastConfigMapName, configMapKey, expectedContent)
Expect(err).ToNot(HaveOccurred(), fmt.Sprintf("Failed to verify Feast ConfigMap %s content: %v", feastConfigMapName, err))
fmt.Printf("Feast ConfigMap %s verified successfully with project and auth type\n", feastConfigMapName)
}

// Parameterized test function that handles both auth and non-auth scenarios
runFeastWorkbenchIntegration := func(authEnabled bool) {
// Apply permissions only if auth is enabled
Expand All @@ -51,8 +106,15 @@ var _ = Describe("Feast Workbench Integration Connection Testing", Ordered, func
utils.ApplyFeastPermissions(permissionFile, "/feast-data/credit_scoring_local/feature_repo/permissions.py", namespace, feastDeploymentName)
}

// Use the shared RunNotebookTest function for common setup and execution
utils.RunNotebookTest(namespace, configMapName, notebookFile, "test/e2e_rhoai/resources/feature_repo", pvcFile, rolebindingName, notebookPVC, notebookName, testDir)
// Create notebook with all setup steps
// Pass feastProject parameter to set the opendatahub.io/feast-config annotation
utils.CreateNotebookTest(namespace, configMapName, notebookFile, "test/e2e_rhoai/resources/feature_repo", pvcFile, rolebindingName, notebookPVC, notebookName, testDir, "credit_scoring_local")

// Verify Feast ConfigMap was created with correct auth type
verifyFeastConfigMap(authEnabled)

// Monitor notebook execution
utils.MonitorNotebookTest(namespace, notebookName)
}

BeforeAll(func() {
Expand Down
11 changes: 8 additions & 3 deletions infra/feast-operator/test/e2e_rhoai/feast_wb_milvus_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ var _ = Describe("Feast Jupyter Notebook Testing", Ordered, func() {
namespace = "test-ns-feast-wb"
configMapName = "feast-wb-cm"
rolebindingName = "rb-feast-test"
notebookFile = "test/e2e_rhoai/resources/feast-test.ipynb"
notebookFile = "test/e2e_rhoai/resources/feast-wb-milvus-test.ipynb"
pvcFile = "test/e2e_rhoai/resources/pvc.yaml"
notebookPVC = "jupyterhub-nb-kube-3aadmin-pvc"
testDir = "/test/e2e_rhoai"
notebookName = "feast-test.ipynb"
notebookName = "feast-wb-milvus-test.ipynb"
feastMilvusTest = "TestFeastMilvusNotebook"
)

Expand All @@ -54,7 +54,12 @@ var _ = Describe("Feast Jupyter Notebook Testing", Ordered, func() {

Context("Feast Jupyter Notebook Test", func() {
It("Should create and run a "+feastMilvusTest+" successfully", func() {
utils.RunNotebookTest(namespace, configMapName, notebookFile, "test/e2e_rhoai/resources/feature_repo", pvcFile, rolebindingName, notebookPVC, notebookName, testDir)
// Create notebook with all setup steps
// Pass empty string for feastProject to keep annotation empty
utils.CreateNotebookTest(namespace, configMapName, notebookFile, "test/e2e_rhoai/resources/feature_repo", pvcFile, rolebindingName, notebookPVC, notebookName, testDir, "")

// Monitor notebook execution
utils.MonitorNotebookTest(namespace, notebookName)
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,12 @@ var _ = Describe("Feast Jupyter Notebook Testing with Ray Offline Store", Ordere

Context("Feast Jupyter Notebook Test with Ray Offline store", func() {
It("Should create and run a "+feastRayTest+" successfully", func() {
utils.RunNotebookTest(namespace, configMapName, notebookFile, "test/e2e_rhoai/resources/feature_repo", pvcFile, rolebindingName, notebookPVC, notebookName, testDir)
// Create notebook with all setup steps
// Pass empty string for feastProject to keep annotation empty
utils.CreateNotebookTest(namespace, configMapName, notebookFile, "test/e2e_rhoai/resources/feature_repo", pvcFile, rolebindingName, notebookPVC, notebookName, testDir, "")

// Monitor notebook execution
utils.MonitorNotebookTest(namespace, notebookName)
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ metadata:
notebooks.opendatahub.io/last-size-selection: Small
opendatahub.io/link: https://jupyter-nb-kube-3aadmin-{{.Namespace}}.{{.IngressDomain}}/notebook/{{.Namespace}}/jupyter-nb-kube-3aadmin
opendatahub.io/username: {{.Username}}
opendatahub.io/feast-config: {{.FeastProject}}
generation: 1
labels:
app: jupyter-nb-kube-3aadmin
opendatahub.io/dashboard: "true"
opendatahub.io/odh-managed: "true"
opendatahub.io/user: {{.Username}}
opendatahub.io/feast-integration: 'true'
name: jupyter-nb-kube-3aadmin
namespace: {{.Namespace}}
spec:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,45 @@
"print(f\"✅ Found Expected Feast version: {actual_version} in workbench\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# --- Configuration Variables ---\n",
"import os \n",
"\n",
"# Fetch token and server directly from oc CLI\n",
"import subprocess\n",
"\n",
"def oc(cmd):\n",
" return subprocess.check_output(cmd, shell=True).decode(\"utf-8\").strip()\n",
"\n",
"token = oc(\"oc whoami -t\")\n",
"server = oc(\"oc whoami --show-server\")\n",
"namespace = os.environ.get(\"NAMESPACE\")\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"!oc login --token=$token --server=$server"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Add user permission to namespace\n",
"!oc adm policy add-role-to-user admin $(oc whoami) -n $namespace"
]
},
{
"cell_type": "code",
"execution_count": null,
Expand All @@ -34,7 +73,7 @@
").read()\n",
"\n",
"# Save the configmap data into an environment variable (if needed)\n",
"os.environ[\"CONFIGMAP_DATA\"] = yaml_content\n"
"os.environ[\"CONFIGMAP_DATA\"] = yaml_content"
]
},
{
Expand All @@ -43,186 +82,8 @@
"metadata": {},
"outputs": [],
"source": [
"#!/usr/bin/env python3\n",
"import os\n",
"import json\n",
"import yaml\n",
"from pathlib import Path\n",
"from typing import Dict, List, Any, Optional\n",
"from feast import FeatureStore\n",
"from feast.repo_config import RepoConfig\n",
"\n",
"def create_feature_store_yaml(config_content: str, config_name: str) -> str:\n",
" \"\"\"\n",
" Create a feature_store.yaml file from config content.\n",
" \n",
" Args:\n",
" config_content: YAML content as string\n",
" config_name: Name identifier for the config (used for filename)\n",
" \n",
" Returns:\n",
" Path to the created YAML file\n",
" \"\"\"\n",
" # Parse the YAML content to validate it\n",
" try:\n",
" config_dict = yaml.safe_load(config_content)\n",
" except yaml.YAMLError as e:\n",
" raise ValueError(f\"Failed to parse YAML content for {config_name}: {e}\")\n",
" \n",
" # Ensure required fields are present\n",
" required_fields = ['project', 'registry', 'provider']\n",
" for field in required_fields:\n",
" if field not in config_dict:\n",
" raise ValueError(f\"Failed to create config {config_name}: missing required field '{field}'\")\n",
" \n",
" # Create filename\n",
" filename = f\"feature_store_{config_name}.yaml\"\n",
" filepath = Path(filename)\n",
" \n",
" # Write the YAML file\n",
" with open(filepath, 'w') as f:\n",
" yaml.dump(config_dict, f, default_flow_style=False, sort_keys=False)\n",
" \n",
" return str(filepath)\n",
"\n",
"\n",
"def create_feature_store_object(yaml_file_path: str) -> FeatureStore:\n",
" \"\"\"\n",
" Create a FeatureStore object from a YAML file.\n",
" \n",
" Args:\n",
" yaml_file_path: Path to the feature_store.yaml file\n",
" \n",
" Returns:\n",
" FeatureStore object\n",
" \"\"\"\n",
" try:\n",
" # Create FeatureStore from the YAML file\n",
" fs = FeatureStore(fs_yaml_file=Path(yaml_file_path))\n",
" return fs\n",
" except Exception as e:\n",
" raise RuntimeError(f\"Failed to create FeatureStore object from {yaml_file_path}: {e}\")\n",
"\n",
"\n",
"def process_client_configs(client_configs: Dict[str, str]) -> Dict[str, Dict[str, Any]]:\n",
" \"\"\"\n",
" Process multiple client config YAML contents and create feature stores.\n",
" \n",
" Args:\n",
" client_configs: Dictionary mapping config names to YAML content strings\n",
" \n",
" Returns:\n",
" Dictionary with results for each config\n",
" \"\"\"\n",
" results = {}\n",
" created_yamls = []\n",
" feature_stores = {}\n",
" \n",
" print(\"Creating feature store YAMLs and objects...\")\n",
" print(\"=\" * 50)\n",
" \n",
" for config_name, config_content in client_configs.items():\n",
" try:\n",
" print(f\"\\nProcessing config: {config_name}\")\n",
"\n",
" # Create YAML file\n",
" yaml_path = create_feature_store_yaml(config_content, config_name)\n",
" created_yamls.append(yaml_path)\n",
" print(f\"✓ Created YAML file: {yaml_path}\")\n",
" \n",
" # Create FeatureStore object\n",
" fs = create_feature_store_object(yaml_path)\n",
" fs_var_name = f\"fs_{fs.project}\"\n",
" globals()[fs_var_name] = fs\n",
" feature_stores[config_name] = fs_var_name\n",
" print(f\"✓ Created FeatureStore object: {fs_var_name}\")\n",
"\n",
" results[config_name] = {\n",
" 'yaml_path': yaml_path,\n",
" 'feature_store': fs_var_name,\n",
" 'project_name': fs.project,\n",
" 'success': True,\n",
" 'error': None\n",
" }\n",
" \n",
" except Exception as e:\n",
" print(f\"✗ Failed to process config {config_name}: {e}\")\n",
" results[config_name] = {\n",
" 'yaml_path': None,\n",
" 'feature_store': None,\n",
" 'project_name': None,\n",
" 'success': False,\n",
" 'error': str(e)\n",
" }\n",
" \n",
" return results\n",
"\n",
"\n",
"def print_summary(results: Dict[str, Dict[str, Any]]) -> None:\n",
" \"\"\"\n",
" Print summary of all operations.\n",
" \n",
" Args:\n",
" results: Results dictionary from process_client_configs\n",
" \"\"\"\n",
" print(\"\\n\" + \"=\" * 50)\n",
" print(\"SUMMARY\")\n",
" print(\"=\" * 50)\n",
"\n",
" successful_configs = [name for name, result in results.items() if result['success']]\n",
" failed_configs = [name for name, result in results.items() if not result['success']]\n",
" print(f\"\\n\\n✓✓Feature Store YAML files have been created in: {os.getcwd()}\")\n",
" print(f\"\\n✓ Successfully processed {len(successful_configs)} config(s):\")\n",
" for config_name in successful_configs:\n",
" result = results[config_name]\n",
" print(f\" - {config_name}: {result['yaml_path']} (Project: {result['project_name']})\")\n",
"\n",
" if failed_configs:\n",
" print(f\"\\n✗ Failed to process {len(failed_configs)} config(s):\")\n",
" for config_name in failed_configs:\n",
" result = results[config_name]\n",
" print(f\" - {config_name}: {result['error']}\")\n",
"\n",
" print(f\"\\n\\n✓✓ Feature Store Object(s) details:\")\n",
" for config_name in successful_configs:\n",
" result = results[config_name]\n",
" print(f\"> Object Name - {result['feature_store']} ; project name - {result['project_name']} ; yaml path - {result['yaml_path']}\")\n",
"\n",
" print(\"\\n\")\n",
" print(\"=\" * 25, \"Usage:\", \"=\" * 25)\n",
" print(\"You can now use feature store object(s) to access the feature store resources and functions!\")\n",
" print(\"\\n// Note: Replace object_name with the actual object name from the list above.\")\n",
" print(\"object_name.list_features()\\nobject_name.get_historical_features()\")\n",
" print(\"=\" * 58)\n",
"\n",
"\n",
"def main():\n",
" \"\"\"\n",
" Main function to demonstrate usage with example configs.\n",
" \"\"\"\n",
" yaml_content = os.getenv(\"CONFIGMAP_DATA\")\n",
"\n",
" if not yaml_content:\n",
" raise ValueError(\"CONFIGMAP_DATA environment variable is not set.\")\n",
"\n",
" # Use environment YAML as config\n",
" client_configs = {\n",
" \"feast_credit_scoring_client\": yaml_content\n",
" }\n",
"\n",
" print(\"=\" * 50)\n",
" print(\"This script will create feature store YAMLs and objects from client configs.\")\n",
" print(f\"Processing {len(client_configs)} selected configurations...\")\n",
" \n",
" # Process the configs\n",
" results = process_client_configs(client_configs)\n",
" \n",
" # Print summary\n",
" print_summary(results)\n",
"\n",
"\n",
"if __name__ == \"__main__\":\n",
" main()"
"fs_credit_scoring_local = FeatureStore(fs_yaml_file='/opt/app-root/src/feast-config/credit_scoring_local')"
]
},
{
Expand Down
Loading
Loading