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
10 changes: 8 additions & 2 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
# Test directories are not coverage subjects. *tests*/* catches mock_tests/;
# *integration/* is excluded too because integration tests run against live
# clusters and version-gated suites (e.g. namespaces, which need Weaviate 1.38+)
# are skipped in the coverage job, which would otherwise count their unexecuted
# bodies as missing patch coverage. Excluding the test files does not affect the
# coverage they contribute to weaviate/ (measured via --cov=weaviate).
[run]
omit = *tests*/*,*__init__.py,weaviate/proto/**
omit = *tests*/*,*integration/*,*__init__.py,weaviate/proto/**


[report]
omit = *tests*/*,*__init__.py
omit = *tests*/*,*integration/*,*__init__.py
exclude_lines =
pragma: not covered
@overload
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ env:
WEAVIATE_135: 1.35.18
WEAVIATE_136: 1.36.12
WEAVIATE_137: 1.37.5-e0fe0d5.amd64
WEAVIATE_138: 1.38.0-rc.0-381222f.amd64

jobs:
lint-and-format:
Expand Down Expand Up @@ -320,7 +321,8 @@ jobs:
$WEAVIATE_134,
$WEAVIATE_135,
$WEAVIATE_136,
$WEAVIATE_137
$WEAVIATE_137,
$WEAVIATE_138
]
steps:
- name: Checkout
Expand Down
2 changes: 1 addition & 1 deletion ci/compose.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ function compose_down_all {
}

function all_weaviate_ports {
echo "8090 8093 8087 8088 8089 8086 8082 8083 8075 8092 8085 8080" # in alphabetic order of appearance in docker-compose files
echo "8090 8093 8087 8088 8089 8086 8094 8082 8083 8075 8092 8085 8080" # in alphabetic order of appearance in docker-compose files
}

function wait(){
Expand Down
45 changes: 45 additions & 0 deletions ci/docker-compose-namespaces.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
version: '3.4'
services:
weaviate-namespaces:
command:
- --host
- 0.0.0.0
- --port
- '8085'
- --scheme
- http
- --write-timeout=600s
image: semitechnologies/weaviate:${WEAVIATE_VERSION}
ports:
- 8094:8085
- 50064:50051
restart: on-failure:0
environment:
# Namespaces feature — requires GraphQL disabled
NAMESPACES_ENABLED: "true"
# Sweep deleting namespaces quickly so delete-then-gone tests stay fast
# (defaults to 30s).
NAMESPACE_CLEANUP_INTERVAL: "2s"
REPLICATION_MAXIMUM_FACTOR: "1"
DISABLE_GRAPHQL: "true"
# Static API key auth (operator-level access)
AUTHENTICATION_APIKEY_ENABLED: "true"
AUTHENTICATION_APIKEY_ALLOWED_KEYS: "admin-key,custom-key"
AUTHENTICATION_APIKEY_USERS: "admin-user,custom-user"
# Anonymous access must be off when RBAC is on
AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: "false"
# RBAC — required for namespace-scoped permission checks
AUTHORIZATION_ENABLE_RBAC: "true"
AUTHORIZATION_ADMIN_USERS: "admin-user"
# Dynamic DB users — needed to create namespaced users
AUTHENTICATION_DB_USERS_ENABLED: "true"
# Storage / cluster
PERSISTENCE_DATA_PATH: "./data-weaviate-0"
CLUSTER_HOSTNAME: "node1"
CLUSTER_IN_LOCALHOST: "true"
CLUSTER_GOSSIP_BIND_PORT: "7102"
CLUSTER_DATA_BIND_PORT: "7103"
RAFT_BOOTSTRAP_EXPECT: "1"
ENABLE_MODULES: ""
...
180 changes: 180 additions & 0 deletions integration/test_namespaces.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import time

import pytest

import weaviate
from integration.conftest import ClientFactory
from weaviate.auth import Auth
from weaviate.namespaces.models import Namespace
from weaviate.rbac.models import Permissions

NS_PORTS = (8094, 50064)
ADMIN_KEY = Auth.api_key("admin-key")

_MINIMUM_VERSION = (1, 38, 0)


def _skip_if_unsupported(client: weaviate.WeaviateClient) -> None:
major, minor, patch = _MINIMUM_VERSION
if client._connection._weaviate_version.is_lower_than(major, minor, patch):
pytest.skip(f"Namespaces require Weaviate {major}.{minor}.{patch}+")


def _a_storage_candidate(client: weaviate.WeaviateClient) -> str:
"""Return a node name usable as a ``home_node`` (must be a real storage candidate)."""
nodes = client.cluster.nodes()
assert nodes, "expected at least one cluster node"
return nodes[0].name


def test_create_and_get_namespace(client_factory: ClientFactory) -> None:
with client_factory(ports=NS_PORTS, auth_credentials=ADMIN_KEY) as client:
_skip_if_unsupported(client)

client.namespaces.create(name="testns")
try:
ns = client.namespaces.get(name="testns")
assert ns is not None
assert isinstance(ns, Namespace)
assert ns.name == "testns"
finally:
client.namespaces.delete(name="testns")


def test_create_namespace_with_home_node(client_factory: ClientFactory) -> None:
with client_factory(ports=NS_PORTS, auth_credentials=ADMIN_KEY) as client:
_skip_if_unsupported(client)

home_node = _a_storage_candidate(client)
ns = client.namespaces.create(name="homenodens", home_node=home_node)
try:
assert ns.name == "homenodens"
assert ns.home_node == home_node
assert ns.state == "active"

fetched = client.namespaces.get(name="homenodens")
assert fetched is not None
assert fetched.home_node == home_node
finally:
client.namespaces.delete(name="homenodens")


def test_update_namespace_home_node(client_factory: ClientFactory) -> None:
with client_factory(ports=NS_PORTS, auth_credentials=ADMIN_KEY) as client:
_skip_if_unsupported(client)

home_node = _a_storage_candidate(client)
# Create without a home_node so the cluster auto-selects, then pin it explicitly.
client.namespaces.create(name="updatens")
try:
updated = client.namespaces.update(name="updatens", home_node=home_node)
assert updated.name == "updatens"
assert updated.home_node == home_node

fetched = client.namespaces.get(name="updatens")
assert fetched is not None
assert fetched.home_node == home_node
finally:
client.namespaces.delete(name="updatens")


def test_get_nonexistent_namespace_returns_none(client_factory: ClientFactory) -> None:
with client_factory(ports=NS_PORTS, auth_credentials=ADMIN_KEY) as client:
_skip_if_unsupported(client)

result = client.namespaces.get(name="doesnotexist")
assert result is None


def test_list_namespaces(client_factory: ClientFactory) -> None:
with client_factory(ports=NS_PORTS, auth_credentials=ADMIN_KEY) as client:
_skip_if_unsupported(client)

client.namespaces.create(name="listns1")
client.namespaces.create(name="listns2")
try:
namespaces = client.namespaces.list_all()
names = [ns.name for ns in namespaces]
assert "listns1" in names
assert "listns2" in names
finally:
client.namespaces.delete(name="listns1")
client.namespaces.delete(name="listns2")


def test_delete_namespace(client_factory: ClientFactory) -> None:
with client_factory(ports=NS_PORTS, auth_credentials=ADMIN_KEY) as client:
_skip_if_unsupported(client)

client.namespaces.create(name="deletens")
client.namespaces.delete(name="deletens")

# Deletion is asynchronous: delete() returns 202 and the server marks the
# namespace "deleting", then removes it on the background cleanup sweep
# (NAMESPACE_CLEANUP_INTERVAL). Poll until it is gone.
deadline = time.time() + 60
fetched = client.namespaces.get(name="deletens")
while fetched is not None and time.time() < deadline:
assert fetched.state == "deleting"
time.sleep(0.5)
fetched = client.namespaces.get(name="deletens")
assert fetched is None


def test_create_namespaced_user(client_factory: ClientFactory) -> None:
with client_factory(ports=NS_PORTS, auth_credentials=ADMIN_KEY) as client:
_skip_if_unsupported(client)

client.namespaces.create(name="usernstest")
# On namespace-enabled clusters an operator creates a user with a
# namespace-qualified id "<namespace>:<user>"; the same qualified id is
# used for get/delete.
qualified_id = "usernstest:nsuser1"
try:
api_key = client.users.db.create(user_id=qualified_id)
assert isinstance(api_key, str)
assert len(api_key) > 0

user = client.users.db.get(user_id=qualified_id)
assert user is not None
assert user.user_id == qualified_id
assert user.namespace == "usernstest"
finally:
client.users.db.delete(user_id=qualified_id)
client.namespaces.delete(name="usernstest")


def test_namespace_permission_manage(client_factory: ClientFactory) -> None:
with client_factory(ports=NS_PORTS, auth_credentials=ADMIN_KEY) as client:
_skip_if_unsupported(client)

client.roles.create(
role_name="ns-manager",
permissions=Permissions.namespaces(namespace="*", manage=True),
)
try:
fetched = client.roles.get(role_name="ns-manager")
assert fetched is not None
assert any(p.namespace == "*" for p in fetched.namespaces_permissions)
finally:
client.roles.delete("ns-manager")


def test_namespace_permission_multiple_namespaces(
client_factory: ClientFactory,
) -> None:
with client_factory(ports=NS_PORTS, auth_credentials=ADMIN_KEY) as client:
_skip_if_unsupported(client)

client.roles.create(
role_name="ns-multi",
permissions=Permissions.namespaces(namespace=["ns1", "ns2"], manage=True),
)
try:
fetched = client.roles.get(role_name="ns-multi")
assert fetched is not None
ns_names = {p.namespace for p in fetched.namespaces_permissions}
assert "ns1" in ns_names
assert "ns2" in ns_names
finally:
client.roles.delete("ns-multi")
Loading
Loading