Skip to content

Commit 5f99a8d

Browse files
* Adding the option to pass public certificate to pass as part of the feature_store.yaml so that we can test self-signed certificate.
* Adding the integration test to run the remote online server in SSL and non SSL mode. Signed-off-by: lrangine <19699092+lokeshrangineni@users.noreply.github.com> Signed-off-by: lrangine <19699092+lokeshrangineni@users.noreply.github.com>
1 parent 8516bf5 commit 5f99a8d

File tree

6 files changed

+171
-86
lines changed

6 files changed

+171
-86
lines changed

sdk/python/feast/infra/online_stores/remote.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ class RemoteOnlineStoreConfig(FeastConfigBaseModel):
4141
""" str: Path to metadata store.
4242
If type is 'remote', then this is a URL for registry server """
4343

44+
ssl_cert_path: StrictStr = ""
45+
""" str: Path to the public certificate path in case if the online server starts in SSL mode. This may be needed especially if it is a self signed certificate, typically this file ends with .crt, .cer, or .pem.
46+
If type is 'remote', then this configuration is needed to connect to remote online server in SSL mode. """
47+
4448

4549
class RemoteOnlineStore(OnlineStore):
4650
"""
@@ -170,6 +174,13 @@ def teardown(
170174
def get_remote_online_features(
171175
session: requests.Session, config: RepoConfig, req_body: str
172176
) -> requests.Response:
173-
return session.post(
174-
f"{config.online_store.path}/get-online-features", data=req_body
175-
)
177+
if config.online_store.ssl_cert_path:
178+
return session.post(
179+
f"{config.online_store.path}/get-online-features",
180+
data=req_body,
181+
verify=config.online_store.ssl_cert_path,
182+
)
183+
else:
184+
return session.post(
185+
f"{config.online_store.path}/get-online-features", data=req_body
186+
)

sdk/python/tests/integration/online_store/test_remote_online_store.py

Lines changed: 71 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@
1515
start_feature_server,
1616
)
1717
from tests.utils.cli_repo_creator import CliRunner
18+
from tests.utils.generate_self_signed_certifcate_util import generate_self_signed_cert
1819
from tests.utils.http_server import free_port
1920

2021

21-
@pytest.mark.parametrize("run_ssl", [True, False])
22+
@pytest.mark.parametrize("ssl_mode", [True, False])
2223
@pytest.mark.integration
23-
def test_remote_online_store_read(auth_config, run_ssl):
24+
def test_remote_online_store_read(auth_config, ssl_mode):
2425
with tempfile.TemporaryDirectory() as remote_server_tmp_dir, tempfile.TemporaryDirectory() as remote_client_tmp_dir:
2526
permissions_list = [
2627
Permission(
@@ -42,12 +43,12 @@ def test_remote_online_store_read(auth_config, run_ssl):
4243
actions=[AuthzedAction.READ_ONLINE],
4344
),
4445
]
45-
server_store, server_url, registry_path = (
46+
server_store, server_url, registry_path, ssl_cert_path = (
4647
_create_server_store_spin_feature_server(
4748
temp_dir=remote_server_tmp_dir,
4849
auth_config=auth_config,
4950
permissions_list=permissions_list,
50-
run_ssl=run_ssl,
51+
ssl_mode=ssl_mode,
5152
)
5253
)
5354
assert None not in (server_store, server_url, registry_path)
@@ -56,6 +57,7 @@ def test_remote_online_store_read(auth_config, run_ssl):
5657
server_registry_path=str(registry_path),
5758
feature_server_url=server_url,
5859
auth_config=auth_config,
60+
ssl_cert_path=ssl_cert_path,
5961
)
6062
assert client_store is not None
6163
_assert_non_existing_entity_feature_views_entity(
@@ -161,23 +163,46 @@ def _assert_client_server_online_stores_are_matching(
161163

162164

163165
def _create_server_store_spin_feature_server(
164-
temp_dir, auth_config: str, permissions_list, run_ssl: bool
166+
temp_dir, auth_config: str, permissions_list, ssl_mode: bool
165167
):
166168
store = default_store(str(temp_dir), auth_config, permissions_list)
167169
feast_server_port = free_port()
170+
if ssl_mode:
171+
certificates_path = tempfile.mkdtemp()
172+
ssl_key_path = os.path.join(certificates_path, "key.pem")
173+
ssl_cert_path = os.path.join(certificates_path, "cert.pem")
174+
generate_self_signed_cert(cert_path=ssl_cert_path, key_path=ssl_key_path)
175+
else:
176+
ssl_key_path = ""
177+
ssl_cert_path = ""
178+
168179
server_url = next(
169180
start_feature_server(
170181
repo_path=str(store.repo_path),
171182
server_port=feast_server_port,
172-
run_ssl=run_ssl,
183+
ssl_key_path=ssl_key_path,
184+
ssl_cert_path=ssl_cert_path,
173185
)
174186
)
175-
print(f"Server started successfully, {server_url}")
176-
return store, server_url, os.path.join(store.repo_path, "data", "registry.db")
187+
if ssl_cert_path and ssl_key_path:
188+
print(f"Online Server started successfully in SSL mode, {server_url}")
189+
else:
190+
print(f"Server started successfully, {server_url}")
191+
192+
return (
193+
store,
194+
server_url,
195+
os.path.join(store.repo_path, "data", "registry.db"),
196+
ssl_cert_path,
197+
)
177198

178199

179200
def _create_remote_client_feature_store(
180-
temp_dir, server_registry_path: str, feature_server_url: str, auth_config: str
201+
temp_dir,
202+
server_registry_path: str,
203+
feature_server_url: str,
204+
auth_config: str,
205+
ssl_cert_path: str = "",
181206
) -> FeatureStore:
182207
project_name = "REMOTE_ONLINE_CLIENT_PROJECT"
183208
runner = CliRunner()
@@ -189,27 +214,50 @@ def _create_remote_client_feature_store(
189214
registry_path=server_registry_path,
190215
feature_server_url=feature_server_url,
191216
auth_config=auth_config,
217+
ssl_cert_path=ssl_cert_path,
192218
)
193219

194220
return FeatureStore(repo_path=repo_path)
195221

196222

197223
def _overwrite_remote_client_feature_store_yaml(
198-
repo_path: str, registry_path: str, feature_server_url: str, auth_config: str
224+
repo_path: str,
225+
registry_path: str,
226+
feature_server_url: str,
227+
auth_config: str,
228+
ssl_cert_path: str = "",
199229
):
200230
repo_config = os.path.join(repo_path, "feature_store.yaml")
201231
with open(repo_config, "w") as repo_config:
202-
repo_config.write(
203-
dedent(
204-
f"""
205-
project: {PROJECT_NAME}
206-
registry: {registry_path}
207-
provider: local
208-
online_store:
209-
path: {feature_server_url}
210-
type: remote
211-
entity_key_serialization_version: 2
212-
"""
232+
if ssl_cert_path:
233+
repo_config.write(
234+
dedent(
235+
f"""
236+
project: {PROJECT_NAME}
237+
registry: {registry_path}
238+
provider: local
239+
online_store:
240+
path: {feature_server_url}
241+
type: remote
242+
ssl_cert_path: {ssl_cert_path}
243+
entity_key_serialization_version: 2
244+
"""
245+
)
246+
+ auth_config
247+
)
248+
249+
else:
250+
repo_config.write(
251+
dedent(
252+
f"""
253+
project: {PROJECT_NAME}
254+
registry: {registry_path}
255+
provider: local
256+
online_store:
257+
path: {feature_server_url}
258+
type: remote
259+
entity_key_serialization_version: 2
260+
"""
261+
)
262+
+ auth_config
213263
)
214-
+ auth_config
215-
)

sdk/python/tests/utils/auth_permissions_util.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,11 @@ def default_store(
5555

5656

5757
def start_feature_server(
58-
repo_path: str, server_port: int, metrics: bool = False, run_ssl: bool = False
58+
repo_path: str,
59+
server_port: int,
60+
metrics: bool = False,
61+
ssl_key_path: str = "",
62+
ssl_cert_path: str = "",
5963
):
6064
host = "0.0.0.0"
6165
cmd = [
@@ -68,12 +72,11 @@ def start_feature_server(
6872
str(server_port),
6973
]
7074

71-
if run_ssl:
72-
# TODO: Generate these certificates during the test run.
75+
if ssl_cert_path and ssl_cert_path:
7376
cmd.append("--ssl-key-path")
74-
cmd.append("test_key.pem")
75-
cmd.append("--ssl-key-path")
76-
cmd.append("test_cert.pem")
77+
cmd.append(ssl_key_path)
78+
cmd.append("--ssl-cert-path")
79+
cmd.append(ssl_cert_path)
7780

7881
feast_server_process = subprocess.Popen(
7982
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
@@ -101,12 +104,14 @@ def start_feature_server(
101104
"localhost", 8000
102105
), "Prometheus server is running when it should be disabled."
103106

104-
yield (
107+
online_server_url = (
105108
f"https://localhost:{server_port}"
106-
if run_ssl
109+
if ssl_key_path and ssl_cert_path
107110
else f"http://localhost:{server_port}"
108111
)
109112

113+
yield (online_server_url)
114+
110115
if feast_server_process is not None:
111116
feast_server_process.kill()
112117

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import logging
2+
from datetime import datetime, timedelta
3+
4+
from cryptography import x509
5+
from cryptography.hazmat.backends import default_backend
6+
from cryptography.hazmat.primitives import hashes, serialization
7+
from cryptography.hazmat.primitives.asymmetric import rsa
8+
from cryptography.x509.oid import NameOID
9+
10+
logger = logging.getLogger(__name__)
11+
12+
13+
def generate_self_signed_cert(
14+
cert_path="cert.pem", key_path="key.pem", common_name="localhost"
15+
):
16+
"""
17+
Generate a self-signed certificate and save it to the specified paths.
18+
19+
:param cert_path: Path to save the certificate (PEM format)
20+
:param key_path: Path to save the private key (PEM format)
21+
:param common_name: Common name (CN) for the certificate, defaults to 'localhost'
22+
"""
23+
# Generate private key
24+
key = rsa.generate_private_key(
25+
public_exponent=65537, key_size=2048, backend=default_backend()
26+
)
27+
28+
# Create a self-signed certificate
29+
subject = issuer = x509.Name(
30+
[
31+
x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
32+
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "California"),
33+
x509.NameAttribute(NameOID.LOCALITY_NAME, "San Francisco"),
34+
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Feast"),
35+
x509.NameAttribute(NameOID.COMMON_NAME, common_name),
36+
]
37+
)
38+
39+
certificate = (
40+
x509.CertificateBuilder()
41+
.subject_name(subject)
42+
.issuer_name(issuer)
43+
.public_key(key.public_key())
44+
.serial_number(x509.random_serial_number())
45+
.not_valid_before(datetime.utcnow())
46+
.not_valid_after(
47+
# Certificate valid for 1 year
48+
datetime.utcnow() + timedelta(days=365)
49+
)
50+
.add_extension(
51+
x509.SubjectAlternativeName([x509.DNSName(common_name)]),
52+
critical=False,
53+
)
54+
.sign(key, hashes.SHA256(), default_backend())
55+
)
56+
57+
# Write the private key to a file
58+
with open(key_path, "wb") as f:
59+
f.write(
60+
key.private_bytes(
61+
encoding=serialization.Encoding.PEM,
62+
format=serialization.PrivateFormat.TraditionalOpenSSL,
63+
encryption_algorithm=serialization.NoEncryption(),
64+
)
65+
)
66+
67+
# Write the certificate to a file
68+
with open(cert_path, "wb") as f:
69+
f.write(certificate.public_bytes(serialization.Encoding.PEM))
70+
71+
logger.info(
72+
f"Self-signed certificate and private key have been generated at {cert_path} and {key_path}."
73+
)

sdk/python/tests/utils/test_cert.pem

Lines changed: 0 additions & 24 deletions
This file was deleted.

sdk/python/tests/utils/test_key.pem

Lines changed: 0 additions & 28 deletions
This file was deleted.

0 commit comments

Comments
 (0)