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_id parameter on all space-aware methods

  • Space-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_id use the default space

  • Existing 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:

  1. 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}")
    
  2. Create space if needed:

    try:
        client.spaces.create(
            id="marketing",
            name="Marketing",
            description="Marketing team space"
        )
    except ConflictError:
        # Space already exists
        pass
    
  3. 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:

  1. 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]}")
    
  2. 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")
    
  3. 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_id parameter

  • Many API calls for space validation

Solutions:

  1. 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}"}
        )
    
  2. 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"}
    )
    
  3. 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

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.