Space Support Migration Guide¶
This guide helps you migrate your code to use the new space support features in kibana-py, including the space_id parameter pattern and space-scoped clients.
Overview¶
Space support enables multi-tenancy in Kibana by allowing you to organize resources (connectors, saved objects, etc.) into isolated spaces. The kibana-py library provides consistent space support across all API clients.
What’s New¶
space_idparameter on all space-aware methodsSpace-scoped clients via
client.space("space-id")Automatic space validation with caching for performance
Consistent error handling for space-related errors
Space context in all error messages
What Hasn’t Changed¶
Default space behavior - operations without
space_iduse the default spaceExisting APIs - all methods work exactly as before
Backward compatibility - no breaking changes to existing code
Migration Scenarios¶
Scenario 1: Using Default Space Only¶
No action required. Your existing code continues to work:
from kibana import Kibana
client = Kibana("http://localhost:5601", api_key="your-api-key")
# This works exactly as before - uses default space
connector = client.actions.create(
name="My Connector",
connector_type_id=".index",
config={"index": "logs"}
)
Result: All operations use the default space, no changes needed.
Scenario 2: Adding Space Support to Existing Code¶
Minimal change required. Add space_id parameter to operations:
from kibana import Kibana
client = Kibana("http://localhost:5601", api_key="your-api-key")
# Before: Default space only
connector = client.actions.create(
name="My Connector",
connector_type_id=".index",
config={"index": "logs"}
)
# After: Specify space
connector = client.actions.create(
name="My Connector",
connector_type_id=".index",
config={"index": "logs"},
space_id="marketing" # Now in marketing space
)
Result: Operations execute in the specified space with automatic validation.
Scenario 3: Multiple Operations in Same Space¶
Use space-scoped client. More efficient for multiple operations:
from kibana import Kibana
client = Kibana("http://localhost:5601", api_key="your-api-key")
# Before: Repeat space_id for each operation
connector1 = client.actions.create(
name="Connector 1",
connector_type_id=".index",
config={"index": "logs1"},
space_id="marketing"
)
connector2 = client.actions.create(
name="Connector 2",
connector_type_id=".index",
config={"index": "logs2"},
space_id="marketing"
)
# After: Use space-scoped client
marketing_client = client.space("marketing") # Validates once
connector1 = marketing_client.actions.create(
name="Connector 1",
connector_type_id=".index",
config={"index": "logs1"}
# space_id not needed - automatically uses "marketing"
)
connector2 = marketing_client.actions.create(
name="Connector 2",
connector_type_id=".index",
config={"index": "logs2"}
# space_id not needed - automatically uses "marketing"
)
Result: Better performance with single validation, cleaner code.
Step-by-Step Migration¶
Step 1: Identify Space-Aware Operations¶
Review your code for operations that should be space-scoped:
# Space-aware operations (support space_id parameter):
client.actions.create(...)
client.actions.get(...)
client.actions.get_all(...)
client.actions.update(...)
client.actions.delete(...)
client.actions.execute(...)
client.saved_objects.create(...)
client.saved_objects.get(...)
client.saved_objects.find(...)
client.saved_objects.update(...)
client.saved_objects.delete(...)
client.saved_objects.bulk_create(...)
client.saved_objects.bulk_get(...)
client.saved_objects.bulk_update(...)
client.saved_objects.bulk_delete(...)
# Space management operations:
client.spaces.create(...)
client.spaces.get(...)
client.spaces.get_all(...)
client.spaces.update(...)
client.spaces.delete(...)
Step 2: Choose Migration Pattern¶
Pattern A: Individual space_id Parameters (for occasional space operations)
# Good for: Occasional operations in different spaces
connector = client.actions.create(
name="Marketing Connector",
connector_type_id=".index",
config={"index": "marketing-logs"},
space_id="marketing"
)
saved_obj = client.saved_objects.create(
type="dashboard",
attributes={"title": "Sales Dashboard"},
space_id="sales"
)
Pattern B: Space-Scoped Client (for multiple operations in same space)
# Good for: Multiple operations in the same space
marketing_client = client.space("marketing")
connector = marketing_client.actions.create(
name="Marketing Connector",
connector_type_id=".index",
config={"index": "marketing-logs"}
)
dashboard = marketing_client.saved_objects.create(
type="dashboard",
attributes={"title": "Marketing Dashboard"}
)
visualization = marketing_client.saved_objects.create(
type="visualization",
attributes={"title": "Marketing Chart"}
)
Step 3: Update Error Handling¶
Add space-specific error handling:
from kibana import Kibana
from kibana.exceptions import SpaceNotFoundError, ConflictError
client = Kibana("http://localhost:5601", api_key="your-api-key")
try:
connector = client.actions.create(
name="My Connector",
connector_type_id=".index",
config={"index": "logs"},
space_id="marketing"
)
except SpaceNotFoundError as e:
print(f"Space '{e.space_id}' does not exist")
# Handle space not found
except ConflictError as e:
print(f"Connector with this name already exists in space")
# Handle conflict
except Exception as e:
print(f"Unexpected error: {e}")
# Handle other errors
Step 4: Test Space Isolation¶
Verify that resources are properly isolated by space:
from kibana import Kibana
client = Kibana("http://localhost:5601", api_key="your-api-key")
# Create connector in marketing space
marketing_connector = client.actions.create(
name="Test Connector",
connector_type_id=".index",
config={"index": "test"},
space_id="marketing"
)
# Verify it exists in marketing space
retrieved = client.actions.get(
id=marketing_connector.body["id"],
space_id="marketing"
)
print(f"✓ Found in marketing space: {retrieved.body['name']}")
# Verify it doesn't exist in default space
try:
client.actions.get(id=marketing_connector.body["id"])
print("❌ ERROR: Connector should not be in default space!")
except Exception:
print("✓ Correctly isolated from default space")
# Verify it doesn't exist in sales space
try:
client.actions.get(
id=marketing_connector.body["id"],
space_id="sales"
)
print("❌ ERROR: Connector should not be in sales space!")
except Exception:
print("✓ Correctly isolated from sales space")
Configuration and Performance¶
Space Validation¶
By default, space existence is validated before operations:
# Default: Validates space exists (recommended)
connector = client.actions.create(
name="My Connector",
connector_type_id=".index",
config={"index": "logs"},
space_id="marketing" # Validates "marketing" exists
)
# Disable validation for performance-critical code
fast_client = client.space("marketing", validate=False)
connector = fast_client.actions.create(
name="My Connector",
connector_type_id=".index",
config={"index": "logs"}
# No validation - faster but riskier
)
Validation Caching¶
Space validation results are cached for 5 minutes by default:
# First call: Validates space exists (API call)
connector1 = client.actions.create(
name="Connector 1",
connector_type_id=".index",
config={"index": "logs1"},
space_id="marketing"
)
# Second call: Uses cached validation (no API call)
connector2 = client.actions.create(
name="Connector 2",
connector_type_id=".index",
config={"index": "logs2"},
space_id="marketing" # Cached - much faster
)
The cache is automatically cleared when:
A space is created via
client.spaces.create()A space is deleted via
client.spaces.delete()The cache TTL expires (5 minutes)
Common Migration Patterns¶
Pattern 1: Multi-Tenant Application¶
from kibana import Kibana
def setup_tenant_resources(tenant_id: str):
"""Set up resources for a tenant in their dedicated space."""
client = Kibana("http://localhost:5601", api_key="your-api-key")
# Create space for tenant if it doesn't exist
try:
space = client.spaces.create(
id=f"tenant-{tenant_id}",
name=f"Tenant {tenant_id}",
description=f"Dedicated space for tenant {tenant_id}"
)
except ConflictError:
# Space already exists
pass
# Use space-scoped client for all tenant operations
tenant_client = client.space(f"tenant-{tenant_id}")
# Create tenant-specific resources
connector = tenant_client.actions.create(
name=f"Tenant {tenant_id} Connector",
connector_type_id=".index",
config={"index": f"tenant-{tenant_id}-logs"}
)
dashboard = tenant_client.saved_objects.create(
type="dashboard",
attributes={"title": f"Tenant {tenant_id} Dashboard"}
)
return {
"space_id": f"tenant-{tenant_id}",
"connector_id": connector.body["id"],
"dashboard_id": dashboard.body["id"]
}
Pattern 2: Environment-Based Spaces¶
import os
from kibana import Kibana
def get_environment_client():
"""Get a client scoped to the current environment."""
client = Kibana("http://localhost:5601", api_key="your-api-key")
# Use different spaces for different environments
env = os.getenv("ENVIRONMENT", "development")
space_map = {
"development": "dev",
"staging": "staging",
"production": "prod"
}
space_id = space_map.get(env, "dev")
return client.space(space_id)
# Usage
env_client = get_environment_client()
# All operations automatically use the correct environment space
connector = env_client.actions.create(
name="App Connector",
connector_type_id=".index",
config={"index": "app-logs"}
)
Pattern 3: Cross-Space Operations¶
from kibana import Kibana
def copy_connector_to_space(
client: Kibana,
connector_id: str,
source_space: str,
target_space: str
):
"""Copy a connector from one space to another."""
# Get connector from source space
source_connector = client.actions.get(
id=connector_id,
space_id=source_space
).body
# Create in target space
new_connector = client.actions.create(
name=source_connector["name"],
connector_type_id=source_connector["connector_type_id"],
config=source_connector["config"],
secrets=source_connector.get("secrets", {}),
space_id=target_space
)
return new_connector.body["id"]
# Usage
client = Kibana("http://localhost:5601", api_key="your-api-key")
new_id = copy_connector_to_space(
client,
connector_id="original-id",
source_space="development",
target_space="staging"
)
Common Migration Issues¶
Issue 1: Space Not Found Errors¶
Symptoms:
SpaceNotFoundError: Space 'marketing' not found
Solutions:
Verify space exists:
# List all spaces spaces = client.spaces.get_all() space_ids = [s.body["id"] for s in spaces.body] print(f"Available spaces: {space_ids}")
Create space if needed:
try: client.spaces.create( id="marketing", name="Marketing", description="Marketing team space" ) except ConflictError: # Space already exists pass
Use correct space ID:
# Space IDs are case-sensitive and use kebab-case # Correct: space_id="marketing-team" # Incorrect: space_id="Marketing Team" # Spaces in ID space_id="marketing_team" # Underscores instead of hyphens
Issue 2: Resources Not Found in Expected Space¶
Symptoms:
NotFoundError: Connector not found
Solutions:
Verify resource space:
# List all connectors in space connectors = client.actions.get_all(space_id="marketing") print(f"Connectors in marketing: {[c['name'] for c in connectors.body]}")
Check default space:
# Resource might be in default space try: connector = client.actions.get(id=connector_id) print("Found in default space") except NotFoundError: print("Not in default space")
Search across all spaces:
def find_connector_space(client, connector_id): """Find which space contains a connector.""" spaces = client.spaces.get_all() for space in spaces.body: try: client.actions.get( id=connector_id, space_id=space["id"] ) return space["id"] except NotFoundError: continue return None space_id = find_connector_space(client, "my-connector-id") print(f"Connector found in space: {space_id}")
Issue 3: Performance Impact from Validation¶
Symptoms:
Slow operations when using
space_idparameterMany API calls for space validation
Solutions:
Use space-scoped client (validates once):
# Slow: Validates for each operation for i in range(100): client.actions.create( name=f"Connector {i}", connector_type_id=".index", config={"index": f"logs-{i}"}, space_id="marketing" # Validates 100 times (cached after first) ) # Fast: Validates once marketing_client = client.space("marketing") for i in range(100): marketing_client.actions.create( name=f"Connector {i}", connector_type_id=".index", config={"index": f"logs-{i}"} )
Disable validation for trusted spaces:
# For performance-critical code where you know space exists fast_client = client.space("marketing", validate=False) # No validation overhead connector = fast_client.actions.create( name="Fast Connector", connector_type_id=".index", config={"index": "logs"} )
Pre-warm cache:
# Validate all spaces upfront spaces = ["marketing", "sales", "support"] for space_id in spaces: client.space(space_id) # Validates and caches # Now all operations use cached validation for space_id in spaces: client.actions.create( name=f"Connector for {space_id}", connector_type_id=".index", config={"index": f"{space_id}-logs"}, space_id=space_id # Uses cache )
Testing Your Migration¶
1. Backward Compatibility Test¶
#!/usr/bin/env python3
"""Test that existing code still works without space_id."""
from kibana import Kibana
client = Kibana("http://localhost:5601", api_key="your-api-key")
# This should work exactly as before
connector = client.actions.create(
name="Backward Compat Test",
connector_type_id=".index",
config={"index": "test"}
# No space_id - uses default space
)
print(f"✅ Backward compatibility: Created connector {connector.body['id']}")
# Cleanup
client.actions.delete(id=connector.body["id"])
2. Space Isolation Test¶
#!/usr/bin/env python3
"""Test that resources are properly isolated by space."""
from kibana import Kibana
from kibana.exceptions import NotFoundError
client = Kibana("http://localhost:5601", api_key="your-api-key")
# Create test spaces
for space_id in ["test-space-1", "test-space-2"]:
try:
client.spaces.create(
id=space_id,
name=f"Test Space {space_id}",
description="Temporary test space"
)
except:
pass
# Create connector in space 1
connector = client.actions.create(
name="Isolation Test",
connector_type_id=".index",
config={"index": "test"},
space_id="test-space-1"
)
connector_id = connector.body["id"]
# Verify it exists in space 1
try:
client.actions.get(id=connector_id, space_id="test-space-1")
print("✅ Found in test-space-1")
except NotFoundError:
print("❌ Not found in test-space-1")
# Verify it doesn't exist in space 2
try:
client.actions.get(id=connector_id, space_id="test-space-2")
print("❌ Should not be in test-space-2")
except NotFoundError:
print("✅ Correctly isolated from test-space-2")
# Cleanup
client.actions.delete(id=connector_id, space_id="test-space-1")
for space_id in ["test-space-1", "test-space-2"]:
try:
client.spaces.delete(id=space_id)
except:
pass
3. Performance Test¶
#!/usr/bin/env python3
"""Test performance impact of space validation."""
import time
from kibana import Kibana
client = Kibana("http://localhost:5601", api_key="your-api-key")
# Create test space
try:
client.spaces.create(
id="perf-test",
name="Performance Test",
description="Temporary test space"
)
except:
pass
# Test 1: Individual space_id parameters (with caching)
start = time.time()
for i in range(10):
connector = client.actions.create(
name=f"Perf Test {i}",
connector_type_id=".index",
config={"index": f"test-{i}"},
space_id="perf-test"
)
client.actions.delete(id=connector.body["id"], space_id="perf-test")
time_individual = time.time() - start
# Test 2: Space-scoped client
start = time.time()
perf_client = client.space("perf-test")
for i in range(10):
connector = perf_client.actions.create(
name=f"Perf Test {i}",
connector_type_id=".index",
config={"index": f"test-{i}"}
)
perf_client.actions.delete(id=connector.body["id"])
time_scoped = time.time() - start
# Test 3: No validation
start = time.time()
fast_client = client.space("perf-test", validate=False)
for i in range(10):
connector = fast_client.actions.create(
name=f"Perf Test {i}",
connector_type_id=".index",
config={"index": f"test-{i}"}
)
fast_client.actions.delete(id=connector.body["id"])
time_no_validation = time.time() - start
print(f"Individual space_id: {time_individual:.3f}s")
print(f"Space-scoped client: {time_scoped:.3f}s")
print(f"No validation: {time_no_validation:.3f}s")
# Cleanup
try:
client.spaces.delete(id="perf-test")
except:
pass
Best Practices After Migration¶
1. Use Space-Scoped Clients for Multiple Operations¶
# Good: Single validation, cleaner code
marketing_client = client.space("marketing")
connector = marketing_client.actions.create(...)
dashboard = marketing_client.saved_objects.create(...)
visualization = marketing_client.saved_objects.create(...)
# Avoid: Multiple validations, repetitive code
connector = client.actions.create(..., space_id="marketing")
dashboard = client.saved_objects.create(..., space_id="marketing")
visualization = client.saved_objects.create(..., space_id="marketing")
2. Handle Space Errors Gracefully¶
from kibana.exceptions import SpaceNotFoundError, ConflictError
try:
connector = client.actions.create(
name="My Connector",
connector_type_id=".index",
config={"index": "logs"},
space_id="marketing"
)
except SpaceNotFoundError as e:
# Create space if it doesn't exist
client.spaces.create(
id=e.space_id,
name=e.space_id.title(),
description=f"Auto-created space for {e.space_id}"
)
# Retry operation
connector = client.actions.create(
name="My Connector",
connector_type_id=".index",
config={"index": "logs"},
space_id="marketing"
)
3. Document Space Requirements¶
def create_tenant_dashboard(
client: Kibana,
tenant_id: str,
dashboard_config: dict
) -> str:
"""
Create a dashboard for a tenant.
Args:
client: Kibana client instance
tenant_id: Tenant identifier (used as space ID)
dashboard_config: Dashboard configuration
Returns:
Dashboard ID
Note:
This function requires a space with ID matching tenant_id to exist.
The space should be created before calling this function.
"""
tenant_client = client.space(f"tenant-{tenant_id}")
dashboard = tenant_client.saved_objects.create(
type="dashboard",
attributes=dashboard_config
)
return dashboard.body["id"]
4. Use Consistent Space Naming¶
# Good: Consistent kebab-case naming
space_ids = [
"marketing-team",
"sales-team",
"support-team"
]
# Avoid: Inconsistent naming
space_ids = [
"marketing_team", # Underscores
"SalesTeam", # CamelCase
"support team" # Spaces
]
Rollback Plan¶
If you need to revert to default space only:
Quick Rollback¶
Simply remove space_id parameters:
# With space support
connector = client.actions.create(
name="My Connector",
connector_type_id=".index",
config={"index": "logs"},
space_id="marketing"
)
# Rollback: Remove space_id
connector = client.actions.create(
name="My Connector",
connector_type_id=".index",
config={"index": "logs"}
# No space_id - back to default space
)
Migrate Resources Back to Default Space¶
def migrate_to_default_space(
client: Kibana,
source_space: str
):
"""Migrate all connectors from a space to default space."""
# Get all connectors in source space
connectors = client.actions.get_all(space_id=source_space)
migrated = []
for connector in connectors.body:
# Create in default space
new_connector = client.actions.create(
name=connector["name"],
connector_type_id=connector["connector_type_id"],
config=connector["config"],
secrets=connector.get("secrets", {})
# No space_id - creates in default space
)
# Delete from source space
client.actions.delete(
id=connector["id"],
space_id=source_space
)
migrated.append({
"old_id": connector["id"],
"new_id": new_connector.body["id"],
"name": connector["name"]
})
return migrated
Additional Resources¶
Spaces - Comprehensive space support documentation
SpacesClient - Spaces API reference
Space Examples - Space usage examples
Adding Space Support to New API Clients - Adding space support to new clients
Common Issues - Troubleshooting space-related issues
Summary¶
Space support in kibana-py provides:
✅ Multi-tenancy - Isolate resources by space
✅ Backward compatible - Existing code works unchanged
✅ Consistent API - Same pattern across all clients
✅ Performance optimized - Validation caching and optional validation
✅ Error handling - Clear space-specific errors
✅ Flexible patterns - Individual parameters or scoped clients
Start with your existing code, add space_id parameters where needed, and enjoy the benefits of multi-tenant resource organization in Kibana.