# openshift-python-wrapper

> Manage OpenShift and Kubernetes resources with clean, Pythonic code instead of raw API calls

---

Source: quickstart.md

Let me explore the repository structure and key files to understand the codebase before writing the documentation.

Now let me look at the `NamespacedResource` class and the `get` classmethod for listing resources:

Now let me check the `get_client` with the `utils/client_config.py` for understanding connection options:

Now I have a thorough understanding of the codebase. Let me write the documentation:

Now I have a comprehensive understanding of the codebase. Let me write the guide:

# Installing and Creating Your First Resource

Get up and running with openshift-python-wrapper so you can programmatically create, query, and delete Kubernetes and OpenShift resources from Python. This guide takes you from installation to your first working resource in under 5 minutes.

## Prerequisites

- Python 3.10 or later
- Access to a Kubernetes or OpenShift cluster (with a valid kubeconfig)
- `pip` or another Python package manager

## Quick Example

```bash
pip install openshift-python-wrapper
```

```python
from ocp_resources.namespace import Namespace
from ocp_resources.resource import get_client

client = get_client()

with Namespace(client=client, name="my-first-namespace") as ns:
    ns.wait_for_status(status=Namespace.Status.ACTIVE, timeout=120)
    print(f"Namespace {ns.name} is active: {ns.exists}")
# Namespace is automatically deleted when the block exits
```

That's it — the namespace is created when entering the `with` block and cleaned up when leaving it.

## Step 1: Install the Package

```bash
pip install openshift-python-wrapper
```

> **Tip:** For isolated environments, use a virtual environment:
> ```bash
> python -m venv .venv && source .venv/bin/activate
> pip install openshift-python-wrapper
> ```

## Step 2: Connect to Your Cluster

The `get_client()` function returns a dynamic client connected to your cluster. By default it reads your kubeconfig from the `KUBECONFIG` environment variable or `~/.kube/config`:

```python
from ocp_resources.resource import get_client

client = get_client()
```

You can also point to a specific kubeconfig file:

```python
client = get_client(config_file="/path/to/kubeconfig")
```

Or connect with a token and host directly:

```python
client = get_client(host="https://api.mycluster.example.com:6443", token="sha256~...")
```

> **Note:** For the full set of connection options — including basic auth, in-cluster config, and environment variables — see [Connecting to Clusters](connecting-to-clusters.html).

## Step 3: Create a Resource

There are two ways to create a resource: explicitly with `deploy()`/`clean_up()`, or automatically with a context manager (`with` statement).

### Option A: Context Manager (Recommended)

The context manager automatically deletes the resource when the block exits, which prevents leftover resources:

```python
from ocp_resources.namespace import Namespace
from ocp_resources.resource import get_client

client = get_client()

with Namespace(client=client, name="namespace-example-2") as ns:
    ns.wait_for_status(status=Namespace.Status.ACTIVE, timeout=120)
    assert ns.exists
# Resource is automatically cleaned up here
```

### Option B: Explicit Deploy and Clean Up

Use `deploy()` and `clean_up()` when you need more control over the lifecycle:

```python
from ocp_resources.namespace import Namespace
from ocp_resources.resource import get_client

client = get_client()

ns = Namespace(client=client, name="namespace-example-1")
ns.deploy()
assert ns.exists
ns.clean_up()
```

> **Tip:** Set `teardown=False` on the resource to prevent `clean_up()` from being called when using a context manager. This is useful when you want the resource to persist after the block exits.

## Step 4: Create a Namespaced Resource

Most Kubernetes resources (Pods, ConfigMaps, Deployments, etc.) live inside a namespace. These require both `name` and `namespace`:

```python
from ocp_resources.config_map import ConfigMap
from ocp_resources.resource import get_client

client = get_client()

with ConfigMap(
    client=client,
    name="app-config",
    namespace="default",
    data={"app.properties": "debug=true\nport=8080"},
) as cm:
    assert cm.exists
    print(f"ConfigMap {cm.name} created in namespace {cm.namespace}")
```

## Step 5: Query Resources

Use the `get()` class method to list resources from the cluster. It returns a generator you can iterate over:

```python
from ocp_resources.pod import Pod
from ocp_resources.resource import get_client

client = get_client()

for pod in Pod.get(client=client, namespace="default"):
    print(pod.name)
```

Filter by labels using `label_selector`:

```python
for pod in Pod.get(client=client, label_selector="app=nginx"):
    node = pod.node
    print(f"Pod {pod.name} is running on node {node.name}")
```

Check if a specific resource exists:

```python
from ocp_resources.namespace import Namespace
from ocp_resources.resource import get_client

client = get_client()

ns = Namespace(client=client, name="default")
if ns.exists:
    print("Namespace exists!")
```

> **Note:** For more on querying, filtering, and watching resources, see [Querying and Watching Resources](querying-resources.html).

## Step 6: Delete a Resource

If you didn't use a context manager, call `clean_up()` to delete the resource:

```python
ns = Namespace(client=client, name="my-temp-namespace")
ns.deploy()
# ... do work ...
ns.clean_up()
```

The `clean_up()` method waits for the resource to be fully deleted by default. Pass `wait=False` to return immediately:

```python
ns.clean_up(wait=False)
```

> **Note:** For full details on resource lifecycle management, see [Creating and Managing Resources](creating-and-managing-resources.html).

## Putting It All Together

Here's a complete script that creates a namespace, deploys a ConfigMap inside it, reads it back, and cleans everything up:

```python
from ocp_resources.config_map import ConfigMap
from ocp_resources.namespace import Namespace
from ocp_resources.resource import get_client

client = get_client()

with Namespace(client=client, name="quickstart-demo") as ns:
    ns.wait_for_status(status=Namespace.Status.ACTIVE, timeout=120)

    with ConfigMap(
        client=client,
        name="demo-config",
        namespace="quickstart-demo",
        data={"greeting": "hello from openshift-python-wrapper"},
    ) as cm:
        assert cm.exists
        print(f"Created ConfigMap '{cm.name}' in namespace '{ns.name}'")

        # Query it back
        for config_map in ConfigMap.get(client=client, namespace="quickstart-demo"):
            print(f"  Found: {config_map.name}")
    # ConfigMap cleaned up here
# Namespace cleaned up here
```

## Advanced Usage

### Creating Resources from YAML

You can create any resource from an existing YAML file instead of specifying parameters in Python:

```python
from ocp_resources.config_map import ConfigMap
from ocp_resources.resource import get_client

client = get_client()

cm = ConfigMap(client=client, yaml_file="my-configmap.yaml")
cm.deploy()
```

### Using a Fake Client for Testing

You can run your code without a live cluster by using a fake client — useful for unit tests and local development:

```python
from ocp_resources.config_map import ConfigMap
from ocp_resources.resource import get_client

client = get_client(fake=True)

with ConfigMap(
    client=client,
    name="test-config",
    namespace="default",
    data={"key": "value"},
) as cm:
    cm.create()
    print(f"Created (fake): {cm.name}")
```

> **Note:** See [Testing Without a Cluster Using the Fake Client](testing-without-cluster.html) for full details on the fake client.

### Schema Validation Before Creating

Enable schema validation to catch configuration errors before sending requests to the cluster:

```python
from ocp_resources.pod import Pod
from ocp_resources.resource import get_client

client = get_client()

pod = Pod(
    client=client,
    name="validated-pod",
    namespace="default",
    containers=[{"name": "nginx", "image": "nginx:latest"}],
    schema_validation_enabled=True,  # Validates on create
)
pod.deploy()
```

You can also validate manually without creating:

```python
pod = Pod(
    name="check-me",
    namespace="default",
    containers=[{"name": "nginx", "image": "nginx:latest"}],
)
pod.validate()  # Raises ValidationError if invalid
```

> **Note:** See [Validating Resources Against OpenAPI Schemas](validating-resources.html) for more validation options.

### Waiting for Resource Conditions

After creating a resource, you can wait for it to reach a specific status or condition:

```python
from ocp_resources.namespace import Namespace
from ocp_resources.resource import get_client

client = get_client()

with Namespace(client=client, name="wait-demo") as ns:
    ns.wait_for_status(status=Namespace.Status.ACTIVE, timeout=120)
```

> **Note:** See [Waiting for Resource Conditions and Status](waiting-for-conditions.html) for advanced waiting patterns.

## Troubleshooting

### "KUBECONFIG not found" or connection errors

Make sure your kubeconfig is in the expected location or explicitly set the path:

```bash
export KUBECONFIG=/path/to/kubeconfig
```

Or pass it directly:

```python
client = get_client(config_file="/path/to/kubeconfig")
```

### Resource not found after creation

Some resources take time to become available. Use `wait_for_status()` or `wait()` to confirm the resource is ready before interacting with it:

```python
ns.deploy()
ns.wait_for_status(status=Namespace.Status.ACTIVE, timeout=120)
```

### Missing `name` or `namespace` errors

- Cluster-scoped resources (like `Namespace`) require only `name`.
- Namespaced resources (like `Pod`, `ConfigMap`, `Deployment`) require both `name` and `namespace`.

```python
# Cluster-scoped — only name needed
Namespace(client=client, name="my-ns")

# Namespaced — both name and namespace required
ConfigMap(client=client, name="my-cm", namespace="my-ns")
```

## Related Pages

- [Connecting to Clusters](connecting-to-clusters.html)
- [Creating and Managing Resources](creating-and-managing-resources.html)
- [Querying and Watching Resources](querying-resources.html)
- [Waiting for Resource Conditions and Status](waiting-for-conditions.html)
- [Testing Without a Cluster Using the Fake Client](testing-without-cluster.html)

---

Source: connecting-to-clusters.md

Let me start by exploring the repository structure and the knowledge graph.

Now let me explore the client configuration and authentication code:

Now let me look at how the `client` parameter is used in the Resource class:

Now let me look at how resources accept the client parameter:

Now let me look at the NamespacedResource and how it handles client:

Now let me look at the tests for the client configuration:

Let me also check how environment variables relate to client configuration:

Now let me check how `get_client` is used with `generate_kubeconfig`:

Now let me look at the `conftest.py` for the tests to understand how fake_client is set up:

Let me also check the test file for more patterns of client usage:

Now let me check what environment variables are documented:

Now let me check the `generate_kubeconfig` related tests more carefully and how `DynamicClientWithKubeconfig` is used:

Now let me also look at how `get_client` is imported/used in other contexts to understand its full interface:

Let me look at some example files to see real usage patterns:

Now let me check the `get` classmethod for how client is used:

Now let me check for the `client_configuration` parameter and how `verify_ssl` is used:

Now I have all the information I need. Let me write the documentation page.

# Connecting to Clusters

Establish authenticated connections to Kubernetes and OpenShift clusters so you can create, query, and manage resources with openshift-python-wrapper.

## Prerequisites

- openshift-python-wrapper installed (see [Installing and Creating Your First Resource](quickstart.html))
- Access to a Kubernetes or OpenShift cluster (kubeconfig file, token, or in-cluster credentials)

## Quick Example

```python
from ocp_resources.resource import get_client
from ocp_resources.namespace import Namespace

# Connect using default kubeconfig (~/.kube/config or $KUBECONFIG)
client = get_client()

# Use the client with any resource
for ns in Namespace.get(client=client):
    print(ns.name)
```

## Creating a Client

All cluster connections go through `get_client()`, which returns a Kubernetes `DynamicClient`. Pass this client to every resource you create or query.

### Using the Default Kubeconfig

If you have a kubeconfig at `~/.kube/config` or the `KUBECONFIG` environment variable is set, no arguments are needed:

```python
from ocp_resources.resource import get_client

client = get_client()
```

The lookup order is:

1. The `config_file` parameter (if provided)
2. The `KUBECONFIG` environment variable
3. `~/.kube/config`

### Using a Specific Kubeconfig File

```python
client = get_client(config_file="/path/to/my/kubeconfig")
```

### Selecting a Context

If your kubeconfig has multiple contexts, select one by name:

```python
client = get_client(config_file="/path/to/kubeconfig", context="my-staging-cluster")
```

### Authenticating with a Bearer Token

Connect to a cluster API server directly using a host URL and token:

```python
client = get_client(
    host="https://api.my-cluster.example.com:6443",
    token="sha256~my-bearer-token",
)
```

### Authenticating with Username and Password (Basic Auth)

For OpenShift clusters that support OAuth-based basic authentication:

```python
client = get_client(
    host="https://api.my-cluster.example.com:6443",
    username="my-user",
    password="my-password",
)
```

> **Note:** Basic auth requires all three parameters: `host`, `username`, and `password`. This uses OpenShift's OAuth flow internally — it is not plain HTTP Basic Authentication.

### Using a Kubeconfig Dictionary

Pass a kubeconfig as a Python dictionary instead of a file path:

```python
config_dict = {
    "apiVersion": "v1",
    "kind": "Config",
    "clusters": [{"name": "my-cluster", "cluster": {"server": "https://api.example.com:6443"}}],
    "users": [{"name": "my-user", "user": {"token": "my-token"}}],
    "contexts": [{"name": "my-context", "context": {"cluster": "my-cluster", "user": "my-user"}}],
    "current-context": "my-context",
}

client = get_client(config_dict=config_dict)
```

## Passing the Client to Resources

Once you have a client, pass it to resource constructors and class methods:

```python
from ocp_resources.resource import get_client
from ocp_resources.namespace import Namespace
from ocp_resources.pod import Pod

client = get_client()

# Creating a resource
ns = Namespace(client=client, name="my-namespace")
ns.deploy()

# Querying resources
for pod in Pod.get(client=client, namespace="my-namespace"):
    print(pod.name)

# Using context managers
with Namespace(client=client, name="temp-namespace") as ns:
    ns.wait_for_status(status=Namespace.Status.ACTIVE, timeout=120)
```

> **Warning:** The `client` parameter will become mandatory in the next major release. Always pass it explicitly. If omitted, the library creates a client automatically using the default kubeconfig, but this triggers a `FutureWarning`.

## Environment Variables

`get_client()` respects several environment variables automatically:

| Variable | Purpose | Default |
|---|---|---|
| `KUBECONFIG` | Path to the kubeconfig file | `~/.kube/config` |
| `HTTPS_PROXY` | HTTPS proxy for cluster connections | None |
| `HTTP_PROXY` | HTTP proxy (used if `HTTPS_PROXY` is not set) | None |

> **Tip:** `HTTPS_PROXY` takes precedence over `HTTP_PROXY` when both are set.

For other environment variables that control logging, resource reuse, and teardown behavior, see [Environment Variables and Configuration](environment-variables.html).

## Advanced Usage

### Disabling TLS Verification

For development clusters with self-signed certificates:

```python
client = get_client(
    host="https://api.dev-cluster.example.com:6443",
    token="sha256~my-token",
    verify_ssl=False,
)
```

> **Warning:** Never disable TLS verification in production environments.

### In-Cluster Configuration

When your code runs inside a pod on the cluster (for example, in a CI/CD pipeline or operator), `get_client()` automatically falls back to in-cluster configuration if the kubeconfig-based connection fails:

```python
# Inside a pod — no config_file or host needed
client = get_client()
```

The library first attempts to connect using the kubeconfig. If that fails with a connection error, it loads credentials from the pod's service account token mounted at `/var/run/secrets/kubernetes.io/serviceaccount/`.

### Generating a Kubeconfig File from a Token Connection

When you connect with `host` and `token`, you might need a kubeconfig file on disk (for example, to pass to external tools). Use `generate_kubeconfig=True`:

```python
client = get_client(
    host="https://api.my-cluster.example.com:6443",
    token="sha256~my-token",
    generate_kubeconfig=True,
)

# Access the generated kubeconfig path
print(client.kubeconfig)
```

The generated kubeconfig file:
- Is written to a temporary file with `0o600` permissions
- Is automatically cleaned up when the process exits

If you already passed a `config_file`, the `generate_kubeconfig` flag reuses that file path instead of creating a new one.

### Passing a Custom Client Configuration

For fine-grained control over TLS settings, timeouts, or proxy configuration, pass a `kubernetes.client.Configuration` object:

```python
import kubernetes
from ocp_resources.resource import get_client

config = kubernetes.client.Configuration()
config.verify_ssl = False
config.proxy = "http://my-proxy:8080"

client = get_client(
    host="https://api.my-cluster.example.com:6443",
    token="sha256~my-token",
    client_configuration=config,
)
```

### Connecting to Multiple Clusters

Create separate clients for each cluster and pass them to different resources:

```python
from ocp_resources.resource import get_client
from ocp_resources.namespace import Namespace

prod_client = get_client(config_file="/path/to/prod-kubeconfig")
staging_client = get_client(config_file="/path/to/staging-kubeconfig")

# Query namespaces on both clusters
prod_namespaces = list(Namespace.get(client=prod_client))
staging_namespaces = list(Namespace.get(client=staging_client))
```

### Using the Fake Client for Testing

For unit tests that don't need a real cluster, pass `fake=True`:

```python
client = get_client(fake=True)
```

This returns a fake client that simulates Kubernetes API operations in memory. See [Testing Without a Cluster Using the Fake Client](testing-without-cluster.html) for full details.

## `get_client()` API Reference

```python
from ocp_resources.resource import get_client

get_client(
    config_file: str | None = None,
    config_dict: dict | None = None,
    context: str | None = None,
    client_configuration: kubernetes.client.Configuration | None = None,
    persist_config: bool = True,
    temp_file_path: str | None = None,
    try_refresh_token: bool = True,
    username: str | None = None,
    password: str | None = None,
    host: str | None = None,
    verify_ssl: bool | None = None,
    token: str | None = None,
    fake: bool = False,
    generate_kubeconfig: bool = False,
) -> DynamicClient
```

| Parameter | Type | Description |
|---|---|---|
| `config_file` | `str \| None` | Path to a kubeconfig file |
| `config_dict` | `dict \| None` | Kubeconfig as a Python dictionary |
| `context` | `str \| None` | Name of the kubeconfig context to use |
| `client_configuration` | `Configuration \| None` | Custom `kubernetes.client.Configuration` object |
| `persist_config` | `bool` | Whether to persist config changes back to the kubeconfig file |
| `temp_file_path` | `str \| None` | Path for temporary kubeconfig file (used with `config_dict`) |
| `try_refresh_token` | `bool` | Attempt to refresh the authentication token (for in-cluster config) |
| `username` | `str \| None` | Username for basic authentication |
| `password` | `str \| None` | Password for basic authentication |
| `host` | `str \| None` | Cluster API server URL (e.g., `https://api.example.com:6443`) |
| `verify_ssl` | `bool \| None` | Set to `False` to skip TLS certificate verification |
| `token` | `str \| None` | Bearer token for authentication |
| `fake` | `bool` | Return a fake in-memory client for testing |
| `generate_kubeconfig` | `bool` | Save connection info to a temporary kubeconfig file accessible via `client.kubeconfig` |

**Returns:** `DynamicClient` — a Kubernetes dynamic client ready for resource operations.

**Raises:**
- `kubernetes.config.ConfigException` — if no valid kubeconfig is found and in-cluster config is unavailable
- `urllib3.exceptions.MaxRetryError` — if the cluster is unreachable (before falling back to in-cluster config)
- `ClientWithBasicAuthError` — if basic auth (username/password) fails

## Authentication Method Priority

When multiple authentication parameters are provided, `get_client()` uses this priority order:

1. **Basic auth** — if `username`, `password`, and `host` are all provided
2. **Token auth** — if `host` and `token` are provided
3. **Config dict** — if `config_dict` is provided
4. **Kubeconfig file** — uses `config_file`, `KUBECONFIG` env var, or `~/.kube/config`
5. **In-cluster config** — automatic fallback if the kubeconfig connection fails

## Troubleshooting

**"FutureWarning: 'client' arg will be mandatory"**
You are creating a resource without passing a `client`. Always call `get_client()` first and pass the result:

```python
client = get_client()
ns = Namespace(client=client, name="my-ns")  # Pass client explicitly
```

**Connection refused or timeout errors**
Verify your cluster is reachable and the kubeconfig or token is correct. Check that `HTTPS_PROXY` / `HTTP_PROXY` environment variables are set correctly if you are behind a proxy.

**"KUBECONFIG environment variable set in code is not detected"**
The Kubernetes Python client reads `KUBECONFIG` at module import time. If you set it dynamically in your code, pass the path directly instead:

```python
import os
# Don't rely on: os.environ["KUBECONFIG"] = "/path/to/config"
# Instead, pass it explicitly:
client = get_client(config_file=os.environ.get("KUBECONFIG", "/path/to/config"))
```

**Basic auth fails with `ClientWithBasicAuthError`**
Basic auth uses the OpenShift OAuth flow. Ensure your cluster has a well-known OAuth endpoint at `/.well-known/oauth-authorization-server`. This method does not work with vanilla Kubernetes clusters.

## Related Pages

- [Installing and Creating Your First Resource](quickstart.html)
- [Environment Variables and Configuration](environment-variables.html)
- [Testing Without a Cluster Using the Fake Client](testing-without-cluster.html)
- [Creating and Managing Resources](creating-and-managing-resources.html)
- [Error Handling and Exception Reference](error-handling.html)

---

Source: creating-and-managing-resources.md

Let me start by exploring the repository structure and the knowledge graph.

Now let me look at the YAML-related functions and some example resource files:

Now let me look at some concrete resource examples and tests:

Now let me look at tests for create, update, delete operations and the exceptions:

Now let me look at the exceptions module and the project resource:

Now let me look at how `to_yaml`, `yaml_file`, and `kind_dict` usage works, and the `dry_run` option:

Now let me check how `ensure_exists` and `wait_for_resource` work:

Now let me look at how the `service.py` and `route.py` look to understand namespaced resources better:

Let me also look at the `node_network_configuration_policy.py` for a more complex `deploy/clean_up` example:

Now let me look at the `to_yaml` method and some environment variable usage:

Let me also check the `dry_run` behavior and check for how `label` and `annotations` work:

Now I have a thorough understanding of the codebase. Let me write the documentation.

# Creating and Managing Resources

Deploy Kubernetes and OpenShift resources to your cluster, update them in place, and clean them up reliably using the openshift-python-wrapper's resource lifecycle methods.

## Prerequisites

- `openshift-python-wrapper` installed (see [Installing and Creating Your First Resource](quickstart.html))
- A connected client — either a real cluster connection or a fake client for testing (see [Connecting to Clusters](connecting-to-clusters.html))

## Quick Example

```python
from ocp_resources.namespace import Namespace
from ocp_resources.resource import get_client

client = get_client()

# Create, use, and automatically delete a Namespace
with Namespace(client=client, name="my-app") as ns:
    print(f"{ns.name} is ready")
# Namespace is deleted when the block exits
```

## Creating Resources

Every resource provides three ways to create it: calling `deploy()` directly, using a context manager, or loading from a YAML file.

### Using `deploy()` and `clean_up()`

```python
from ocp_resources.namespace import Namespace

ns = Namespace(client=client, name="my-namespace")
ns.deploy()

# ... do work ...

ns.clean_up()  # Deletes the resource and waits for deletion
```

`deploy()` returns the resource instance, so you can chain:

```python
ns = Namespace(client=client, name="my-namespace").deploy()
```

Pass `wait=True` to `deploy()` to block until the resource reaches its ready state:

```python
ns = Namespace(client=client, name="my-namespace")
ns.deploy(wait=True)
```

### Using a Context Manager

The context manager calls `deploy()` on entry and `clean_up()` on exit, guaranteeing cleanup even if an exception occurs:

```python
from ocp_resources.config_map import ConfigMap

with ConfigMap(
    client=client,
    name="app-config",
    namespace="my-namespace",
    data={"database_url": "postgres://db:5432/myapp"},
) as cm:
    print(cm.instance.data)
# ConfigMap is automatically deleted here
```

> **Note:** If cleanup fails during context manager exit, a `ResourceTeardownError` is raised so failures are never silently ignored.

### Namespaced vs. Cluster-Scoped Resources

Resources that live within a namespace inherit from `NamespacedResource` and require both `name` and `namespace`:

```python
from ocp_resources.pod import Pod

pod = Pod(
    client=client,
    name="nginx",
    namespace="my-namespace",
    containers=[{"name": "nginx", "image": "nginx:latest"}],
)
pod.deploy()
```

Cluster-scoped resources (like `Namespace`) inherit from `Resource` and need only `name`:

```python
from ocp_resources.namespace import Namespace

ns = Namespace(client=client, name="my-namespace")
ns.deploy()
```

### Creating Resources from a YAML File

Pass `yaml_file` to create any resource directly from a YAML manifest. The resource name and namespace are read from the file:

```python
from ocp_resources.config_map import ConfigMap

cm = ConfigMap(client=client, yaml_file="manifests/configmap.yaml")
cm.deploy()
```

`yaml_file` also accepts a `StringIO` object for in-memory YAML:

```python
from io import StringIO
from ocp_resources.config_map import ConfigMap

yaml_content = StringIO("""
apiVersion: v1
kind: ConfigMap
metadata:
  name: dynamic-config
  namespace: my-namespace
data:
  setting: "enabled"
""")

cm = ConfigMap(client=client, yaml_file=yaml_content)
cm.deploy()
```

### Creating Resources from a Dictionary

Use `kind_dict` to pass a pre-built dictionary. When `kind_dict` is provided, the resource class's `to_dict()` logic is bypassed entirely:

```python
from ocp_resources.config_map import ConfigMap

resource_dict = {
    "apiVersion": "v1",
    "kind": "ConfigMap",
    "metadata": {
        "name": "from-dict",
        "namespace": "my-namespace",
    },
    "data": {"key": "value"},
}

cm = ConfigMap(client=client, kind_dict=resource_dict)
cm.deploy()
```

> **Warning:** `yaml_file` and `kind_dict` are mutually exclusive. Passing both raises a `ValueError`.

### Adding Labels and Annotations

Pass `label` and `annotations` at construction time:

```python
from ocp_resources.namespace import Namespace

ns = Namespace(
    client=client,
    name="labeled-ns",
    label={"team": "platform", "env": "staging"},
    annotations={"description": "Staging environment namespace"},
)
ns.deploy()
```

## Updating Resources

### Patching with `update()`

`update()` sends a merge patch — it adds or modifies fields without removing existing ones:

```python
cm = ConfigMap(
    client=client,
    name="app-config",
    namespace="my-namespace",
    ensure_exists=True,
)

cm.update(resource_dict={
    "data": {"new_key": "new_value"},
})
```

### Replacing with `update_replace()`

`update_replace()` performs a full replacement of the resource. Use this when you need to **remove** fields that `update()` would leave untouched:

```python
# Get the current state
current = cm.instance.to_dict()

# Remove a key
current["data"].pop("old_key", None)

# Replace the entire resource
cm.update_replace(resource_dict=current)
```

| Method | HTTP Verb | Behavior | Use When |
|--------|-----------|----------|----------|
| `update()` | PATCH | Merges fields into existing resource | Adding or changing fields |
| `update_replace()` | PUT | Replaces entire resource | Removing fields or doing full replacements |

## Deleting Resources

### Using `clean_up()`

```python
ns.clean_up()  # Deletes and waits for removal (default: wait=True)
```

Control the deletion behavior:

```python
# Delete without waiting
ns.clean_up(wait=False)

# Delete with a custom timeout (in seconds)
ns.clean_up(timeout=120)
```

`clean_up()` returns `True` if the resource was successfully deleted, `False` otherwise.

### Using `delete()`

`delete()` is the lower-level method called by `clean_up()`:

```python
ns.delete(wait=True, timeout=240)
```

You can also pass a custom body for delete options:

```python
ns.delete(body={"propagationPolicy": "Background"})
```

> **Tip:** If you delete a resource that doesn't exist, `delete()` logs a warning and returns `True` instead of raising an error.

### Controlling Teardown

Set `teardown=False` at construction to prevent automatic deletion in context managers:

```python
with Namespace(client=client, name="persistent-ns", teardown=False) as ns:
    print("This namespace will NOT be deleted on exit")
```

## Checking Resource State

### Check If a Resource Exists

```python
ns = Namespace(client=client, name="my-namespace")
if ns.exists:
    print("Namespace exists")
```

### Ensure a Resource Already Exists

Use `ensure_exists=True` to raise `ResourceNotFoundError` if the resource is not present on the cluster:

```python
from ocp_resources.config_map import ConfigMap

# Raises ResourceNotFoundError if the ConfigMap doesn't exist
cm = ConfigMap(
    client=client,
    name="must-exist",
    namespace="default",
    ensure_exists=True,
)
```

### Get the Live Instance

The `instance` property fetches the current state from the API server:

```python
ns = Namespace(client=client, name="my-namespace", ensure_exists=True)
print(ns.instance.metadata.labels)
print(ns.status)
```

### Export as YAML

```python
ns = Namespace(client=client, name="my-namespace")
print(ns.to_yaml())
```

For more on querying, watching, and status checks, see [Querying and Watching Resources](querying-resources.html).

## Dry Run Mode

Validate a resource against the API server without actually creating it:

```python
from ocp_resources.namespace import Namespace

ns = Namespace(client=client, name="dry-run-ns", dry_run=True)
ns.deploy()  # Sends the request with ?dryRun=All — no resource is created
```

## Advanced Usage

### Constructor Parameters Reference

All resource classes accept these common parameters:

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `client` | `DynamicClient` | `None` | Cluster client connection (will be required in next major release) |
| `name` | `str` | `None` | Resource name |
| `namespace` | `str` | `None` | Namespace (namespaced resources only) |
| `teardown` | `bool` | `True` | Whether `clean_up()` deletes the resource |
| `yaml_file` | `str` | `None` | Path to a YAML manifest file |
| `kind_dict` | `dict` | `None` | Dictionary representation of the resource |
| `delete_timeout` | `int` | `240` | Timeout in seconds for delete operations |
| `dry_run` | `bool` | `False` | Server-side dry run without persisting |
| `label` | `dict` | `None` | Labels to set on the resource |
| `annotations` | `dict` | `None` | Annotations to set on the resource |
| `ensure_exists` | `bool` | `False` | Raise if resource doesn't already exist |
| `wait_for_resource` | `bool` | `False` | Wait for resource creation in context manager |
| `schema_validation_enabled` | `bool` | `False` | Validate against OpenAPI schema on create/replace |
| `hash_log_data` | `bool` | `True` | Mask sensitive data in logs |

For the full API reference, see [Resource and NamespacedResource API](resource-api.html).

### Schema Validation on Create and Replace

Enable automatic schema validation so `create()` and `update_replace()` check your resource against its OpenAPI schema before sending it to the API server:

```python
from ocp_resources.config_map import ConfigMap

cm = ConfigMap(
    client=client,
    name="validated-cm",
    namespace="default",
    data={"key": "value"},
    schema_validation_enabled=True,  # Validates before create()
)
cm.deploy()
```

You can also validate on demand or validate a dictionary directly:

```python
# Validate an existing resource instance
cm.validate()

# Validate a dictionary without creating a resource
ConfigMap.validate_dict({
    "apiVersion": "v1",
    "kind": "ConfigMap",
    "metadata": {"name": "test"},
    "data": {"key": "value"},
})
```

Both methods raise `ValidationError` if validation fails. For more details, see [Validating Resources Against OpenAPI Schemas](validating-resources.html).

### Temporary Edits with ResourceEditor

`ResourceEditor` applies patches to existing resources and automatically restores original values when used as a context manager:

```python
from ocp_resources.resource import ResourceEditor

ns = Namespace(client=client, name="my-namespace", ensure_exists=True)

with ResourceEditor(
    patches={ns: {"metadata": {"labels": {"temporary-label": "true"}}}}
):
    # Label is applied
    assert ns.labels["temporary-label"] == "true"
# Label is automatically removed
```

`ResourceEditor` supports both `update` (merge patch) and `replace` actions:

```python
with ResourceEditor(
    patches={ns: {"metadata": {"labels": {"keep-only-this": "true"}}}},
    action="replace",
):
    pass  # Full replacement applied, then restored on exit
```

For complete coverage, see [Editing Resources Temporarily with ResourceEditor](editing-resources-temporarily.html).

### Managing Multiple Resources

#### ResourceList — Create N Copies

Create multiple instances of the same resource type with indexed names:

```python
from ocp_resources.namespace import Namespace
from ocp_resources.resource import ResourceList

with ResourceList(
    client=client,
    resource_class=Namespace,
    name="test-ns",
    num_resources=3,
) as namespaces:
    # Creates test-ns-1, test-ns-2, test-ns-3
    for ns in namespaces:
        print(ns.name)
# All three namespaces are deleted on exit
```

#### NamespacedResourceList — One Per Namespace

Create one resource in each namespace from a `ResourceList`:

```python
from ocp_resources.pod import Pod
from ocp_resources.resource import NamespacedResourceList, ResourceList

namespaces = ResourceList(
    client=client,
    resource_class=Namespace,
    name="env",
    num_resources=3,
)

with NamespacedResourceList(
    client=client,
    resource_class=Pod,
    namespaces=namespaces,
    name="worker",
    containers=[{"name": "app", "image": "myapp:latest"}],
) as pods:
    for pod in pods:
        print(f"{pod.name} in {pod.namespace}")
```

For detailed patterns, see [Managing Bulk Resources with ResourceList](managing-resource-lists.html).

### Debugging with Environment Variables

These environment variables control resource lifecycle behavior at runtime without code changes:

| Variable | Effect |
|----------|--------|
| `REUSE_IF_RESOURCE_EXISTS` | Skip `deploy()` if the resource already exists |
| `SKIP_RESOURCE_TEARDOWN` | Skip `clean_up()` to keep resources for debugging |

Both accept a YAML-formatted dictionary. Spaces are significant:

```bash
# Skip creation of all Pods
export REUSE_IF_RESOURCE_EXISTS="{Pod: {}}"

# Skip creation of a specific Pod in a specific namespace
export REUSE_IF_RESOURCE_EXISTS="{Pod: {my-pod: my-namespace}}"

# Skip teardown for a Namespace and a Pod
export SKIP_RESOURCE_TEARDOWN="{Namespace: {my-ns:}, Pod: {my-pod: my-namespace}}"
```

For the full list of environment variables, see [Environment Variables and Configuration](environment-variables.html).

## Troubleshooting

**`MissingRequiredArgumentError` on construction**
You must provide either `name`, `yaml_file`, or `kind_dict`. For namespaced resources, both `name` and `namespace` are required unless using `yaml_file` or `kind_dict`.

**`ResourceTeardownError` in context manager**
The resource's `clean_up()` returned `False`, meaning deletion failed. Check cluster permissions and resource finalizers.

**`ValueError: yaml_file and resource_dict are mutually exclusive`**
You passed both `yaml_file` and `kind_dict` to a constructor. Use only one.

**`ResourceNotFoundError` with `ensure_exists=True`**
The resource doesn't exist on the cluster. Verify the name (and namespace for namespaced resources) are correct.

**Resource not deleted after context manager exits**
Check if `teardown=False` was passed. Also verify the `SKIP_RESOURCE_TEARDOWN` environment variable is not set.

## Related Pages

- [Installing and Creating Your First Resource](quickstart.html)
- [Querying and Watching Resources](querying-resources.html)
- [Waiting for Resource Conditions and Status](waiting-for-conditions.html)
- [Editing Resources Temporarily with ResourceEditor](editing-resources-temporarily.html)
- [Resource and NamespacedResource API](resource-api.html)

---

Source: querying-resources.md

Let me start by reading the knowledge graph and understanding the codebase structure.

Now let me explore the key source files related to querying, listing, watching, existence checks, and status retrieval.

Now let me look at the `_prepare_resources` method and the `get` method more closely to understand the filtering parameters:

Now let me look at the event module and test files for querying:

Now let me look at the test files for get/list/watch operations:

Now let me check the conftest to understand fake_client and look at the `Condition` attribute and `Status` attribute patterns:

Now let me check how `get` is called with selectors:

Let me look at how `get` passes `field_selector`, `label_selector`, and `namespace` through kwargs:

Now let me look at more examples of how get() is used with various kwargs, and check what other files show:

Let me check how `_build_event_field_selector` and events work:

Now let me check the namespace.py to see how get() is used in a simple resource:

Now let me look at the node.py for a cluster-scoped resource usage of get:

Now let me check the exceptions module for ConditionError:

Let me also check `ensure_exists`:

Let me also look for properties that relate to querying the resource (uuid, labels):

Now let me check the test for events and any resource-specific tests about querying:

Let me also look at the fake_client to understand what kwargs `get` passes through:

Now I have a thorough understanding of the codebase. Let me write the documentation page.

# Querying and Watching Resources

Find, filter, and monitor Kubernetes and OpenShift resources in your cluster — whether you need to list all pods by label, check if a resource exists, watch for real-time changes, or inspect status and conditions.

## Prerequisites

- `openshift-python-wrapper` installed (see [Installing and Creating Your First Resource](quickstart.html))
- A connected `DynamicClient` instance (see [Connecting to Clusters](connecting-to-clusters.html))

## Quick Example

```python
from ocp_resources.pod import Pod
from ocp_resources.resource import get_client

client = get_client()

# List all pods in a namespace
for pod in Pod.get(client=client, namespace="my-namespace"):
    print(f"{pod.name} — {pod.status}")
```

## Listing Resources

Every resource class provides a `get()` class method that returns a generator of resource objects. The method accepts standard Kubernetes query parameters as keyword arguments.

### List all resources of a type

```python
from ocp_resources.namespace import Namespace

for ns in Namespace.get(client=client):
    print(ns.name)
```

### Filter by namespace

Namespaced resources (like `Pod`, `Service`, `ConfigMap`) accept a `namespace` keyword:

```python
from ocp_resources.pod import Pod

for pod in Pod.get(client=client, namespace="default"):
    print(pod.name)
```

### Filter by label selector

Use `label_selector` with standard Kubernetes label selector syntax:

```python
from ocp_resources.pod import Pod

# Single label
for pod in Pod.get(client=client, namespace="my-app", label_selector="app=nginx"):
    print(pod.name)

# Multiple labels (comma-separated)
for pod in Pod.get(client=client, label_selector="app=nginx,tier=frontend"):
    print(pod.name)
```

### Filter by field selector

Use `field_selector` with standard Kubernetes field selector syntax:

```python
from ocp_resources.pod import Pod

# Find pods on a specific node
for pod in Pod.get(client=client, field_selector="spec.nodeName=worker-1"):
    print(pod.name)

# Find pods by status phase
for pod in Pod.get(client=client, namespace="default", field_selector="status.phase=Running"):
    print(pod.name)
```

### Get raw resource objects

By default, `get()` returns wrapper objects. Pass `raw=True` to get the underlying `ResourceInstance` or `ResourceField` objects directly:

```python
from ocp_resources.pod import Pod

for pod in Pod.get(client=client, namespace="default", raw=True):
    # Returns raw ResourceField objects instead of Pod instances
    print(pod.metadata.name, pod.status.phase)
```

### List all cluster resources

To iterate over every resource type in the cluster:

```python
from ocp_resources.resource import Resource

for resource in Resource.get_all_cluster_resources(client=client):
    print(f"{resource.kind}: {resource.metadata.name}")

# Filter with label selector
for resource in Resource.get_all_cluster_resources(client=client, label_selector="my-label=value"):
    print(f"{resource.kind}: {resource.metadata.name}")
```

## `get()` Method Reference

```python
@classmethod
def get(
    cls,
    client: DynamicClient | None = None,
    singular_name: str = "",
    exceptions_dict: dict[type[Exception], list[str]] = DEFAULT_CLUSTER_RETRY_EXCEPTIONS,
    raw: bool = False,
    *args,
    **kwargs,
) -> Generator
```

| Parameter | Type | Description |
|---|---|---|
| `client` | `DynamicClient` | Kubernetes dynamic client (required) |
| `singular_name` | `str` | Resource kind in lowercase; used when multiple resources match the same kind |
| `raw` | `bool` | If `True`, return raw `ResourceInstance`/`ResourceField` objects instead of wrapper instances |
| `exceptions_dict` | `dict` | Exceptions to retry on during API calls |
| `**kwargs` | | Passed through to the Kubernetes API: `namespace`, `label_selector`, `field_selector`, `limit`, `resource_version`, `timeout_seconds`, etc. |

**Returns:** A generator yielding resource objects.

## Checking Resource Existence

The `exists` property queries the API server and returns the resource instance if it exists, or `None` if it does not.

```python
from ocp_resources.namespace import Namespace

ns = Namespace(client=client, name="my-namespace")

if ns.exists:
    print("Namespace exists")
else:
    print("Namespace not found")
```

### Require existence at initialization

Use `ensure_exists=True` to raise `ResourceNotFoundError` immediately if the resource is not found:

```python
from ocp_resources.pod import Pod

# Raises ResourceNotFoundError if the pod doesn't exist
pod = Pod(client=client, name="my-pod", namespace="default", ensure_exists=True)
```

> **Tip:** Use `ensure_exists=True` when you need a reference to a resource that must already be on the cluster, such as a pre-existing ConfigMap or Secret.

## Getting the Resource Instance

The `instance` property fetches the full live resource from the API server:

```python
pod = Pod(client=client, name="my-pod", namespace="default")

# Get the full resource instance
inst = pod.instance
print(inst.metadata.name)
print(inst.metadata.uid)
print(inst.metadata.resourceVersion)

# Convert to a plain dictionary
resource_dict = pod.instance.to_dict()
```

> **Note:** Each access to `.instance` makes an API call. Cache the result in a local variable if you need multiple fields from the same snapshot.

## Retrieving Labels

The `labels` property returns the resource's labels:

```python
ns = Namespace(client=client, name="my-namespace")
print(ns.labels)  # {'kubernetes.io/metadata.name': 'my-namespace', ...}

# Check a specific label
if "app" in ns.labels:
    print(f"App label: {ns.labels['app']}")
```

## Retrieving Resource Status

### Status phase

The `status` property returns the current phase (e.g., `Running`, `Pending`, `Active`):

```python
from ocp_resources.namespace import Namespace

ns = Namespace(client=client, name="my-namespace")
print(ns.status)  # "Active"
```

Each resource class defines status constants you can compare against:

```python
from ocp_resources.pod import Pod

pod = Pod(client=client, name="my-pod", namespace="default")
if pod.status == Pod.Status.RUNNING:
    print("Pod is running")
```

Common status constants available on all resources:

| Constant | Value |
|---|---|
| `Status.RUNNING` | `"Running"` |
| `Status.PENDING` | `"Pending"` |
| `Status.SUCCEEDED` | `"Succeeded"` |
| `Status.FAILED` | `"Failed"` |
| `Status.ACTIVE` | `"Active"` |
| `Status.TERMINATING` | `"Terminating"` |
| `Status.COMPLETED` | `"Completed"` |
| `Status.ERROR` | `"Error"` |

### Condition messages

Use `get_condition_message()` to retrieve the message for a specific condition type:

```python
pod = Pod(client=client, name="my-pod", namespace="default")

# Get message for a condition type
msg = pod.get_condition_message(condition_type="Ready")
print(msg)

# Optionally filter by condition status
msg = pod.get_condition_message(
    condition_type="Ready",
    condition_status="False",
)
print(msg)  # Returns empty string if status doesn't match
```

**Signature:**
```python
def get_condition_message(
    self,
    condition_type: str,
    condition_status: str = "",
) -> str
```

| Parameter | Type | Description |
|---|---|---|
| `condition_type` | `str` | The condition type to look up (e.g., `"Ready"`, `"Available"`) |
| `condition_status` | `str` | If provided, only return the message when the condition has this status |

**Returns:** The condition message string, or `""` if not found or status doesn't match.

### Condition constants

Resources provide built-in condition constants:

```python
from ocp_resources.pod import Pod

pod = Pod(client=client, name="my-pod", namespace="default")

# Use condition constants
msg = pod.get_condition_message(
    condition_type=Pod.Condition.READY,
    condition_status=Pod.Condition.Status.TRUE,
)
```

Common condition constants:

| Constant | Value |
|---|---|
| `Condition.READY` | `"Ready"` |
| `Condition.AVAILABLE` | `"Available"` |
| `Condition.PROGRESSING` | `"Progressing"` |
| `Condition.DEGRADED` | `"Degraded"` |
| `Condition.UPGRADEABLE` | `"Upgradeable"` |
| `Condition.Status.TRUE` | `"True"` |
| `Condition.Status.FALSE` | `"False"` |
| `Condition.Status.UNKNOWN` | `"Unknown"` |

## Watching for Changes

### Watch a specific resource

The `watcher()` method streams events for a specific resource instance:

```python
from ocp_resources.pod import Pod

pod = Pod(client=client, name="my-pod", namespace="default")

for event in pod.watcher(timeout=60):
    print(f"Event type: {event['type']}")       # ADDED, MODIFIED, DELETED
    print(f"Object: {event['object'].metadata.name}")
    print(f"Raw: {event['raw_object']}")
```

**Signature:**
```python
def watcher(
    self,
    timeout: int,
    resource_version: str = "",
) -> Generator[dict[str, Any], None, None]
```

| Parameter | Type | Description |
|---|---|---|
| `timeout` | `int` | How long to watch (in seconds) |
| `resource_version` | `str` | Only receive events newer than this version. Defaults to the resource version at creation time. |

Each yielded event is a dict with these keys:

| Key | Description |
|---|---|
| `type` | Event type: `"ADDED"`, `"MODIFIED"`, or `"DELETED"` |
| `object` | A `ResourceInstance` wrapping the resource |
| `raw_object` | A plain dict representing the resource |

## Querying Events

### Stream events for a resource

The `events()` method watches Kubernetes events associated with a resource:

```python
from ocp_resources.pod import Pod

pod = Pod(client=client, name="my-pod", namespace="default")

for event in pod.events(timeout=30):
    print(f"Reason: {event.object.reason}")
    print(f"Message: {event.object.message}")
```

**Signature:**
```python
def events(
    self,
    name: str = "",
    label_selector: str = "",
    field_selector: str = "",
    resource_version: str = "",
    timeout: int = 240,
) -> Generator
```

| Parameter | Type | Description |
|---|---|---|
| `name` | `str` | Filter by event name |
| `label_selector` | `str` | Filter by labels (comma-separated `key=value`) |
| `field_selector` | `str` | Additional field filters (merged with auto-generated `involvedObject.name` filter) |
| `resource_version` | `str` | Only return events newer than this version |
| `timeout` | `int` | Watch timeout in seconds (default: 240) |

```python
# Example: filter for Warning events with a specific reason
for event in pod.events(
    field_selector="type==Warning,reason==BackOff",
    timeout=10,
):
    print(event.object.message)
```

### List existing events (non-streaming)

Use `Event.list()` to get a snapshot of existing events without streaming:

```python
from ocp_resources.event import Event

# List Warning events from the last 5 minutes
events = Event.list(
    client=client,
    namespace="my-namespace",
    field_selector="type==Warning",
)
for event in events:
    print(f"{event.reason}: {event.message}")

# List events from the last 10 minutes
events = Event.list(
    client=client,
    namespace="my-namespace",
    since_seconds=600,
)
```

**Signature:**
```python
@classmethod
def Event.list(
    cls,
    client: DynamicClient,
    namespace: str | None = None,
    field_selector: str | None = None,
    label_selector: str | None = None,
    since_seconds: int = 300,
) -> list
```

Events are returned sorted by timestamp (most recent first).

### Delete events

Clean up events before tests to avoid false positives:

```python
from ocp_resources.event import Event

Event.delete_events(
    client=client,
    namespace="my-namespace",
    field_selector="reason=AnEventReason",
)
```

## Advanced Usage

### Handling resources with duplicate API groups

Some resource kinds exist in multiple API groups. Use `singular_name` to disambiguate:

```python
from ocp_resources.dns import DNS

# When a kind exists in multiple API groups, pass singular_name
for dns in DNS.get(client=client, singular_name="dns"):
    print(dns.name)
```

### Using `full_api()` for low-level access

The `full_api()` method returns the raw Kubernetes API resource object, giving you direct access to the API:

```python
ns = Namespace(client=client, name="my-namespace")
api = ns.full_api()

# Use the API directly for custom queries
result = api.get(
    label_selector="app=nginx",
    field_selector="status.phase=Active",
    limit=10,
)
```

`full_api()` accepts these keyword arguments:

| Parameter | Description |
|---|---|
| `pretty` | Pretty-print output |
| `_continue` | Continuation token for paginated results |
| `field_selector` | Filter by fields |
| `label_selector` | Filter by labels |
| `limit` | Maximum number of results |
| `resource_version` | Filter by resource version |
| `timeout_seconds` | Server-side timeout |
| `watch` | Enable watch mode |

### Converting resources to YAML

```python
ns = Namespace(client=client, name="my-namespace")
print(ns.to_yaml())
```

### Wait for conditions and status

For polling-based waiting (e.g., wait until a pod is `Running` or a condition is `True`), see [Waiting for Resource Conditions and Status](waiting-for-conditions.html).

### Querying during tests with the fake client

You can use all querying methods with the fake client for unit testing:

```python
from ocp_resources.resource import get_client
from ocp_resources.namespace import Namespace

client = get_client(fake=True)
ns = Namespace(client=client, name="test-ns")
ns.deploy()

# All query methods work the same way
assert ns.exists
assert ns.instance
assert ns.status == Namespace.Status.ACTIVE

# Listing works too
for ns in Namespace.get(client=client):
    print(ns.name)
```

See [Testing Without a Cluster Using the Fake Client](testing-without-cluster.html) for details.

## Troubleshooting

**`ResourceNotFoundError` when using `ensure_exists=True`**
The resource does not exist on the cluster. Verify the name, namespace, and that the resource has been created before initializing the wrapper.

**`NotImplementedError: Couldn't find <Kind> in <api_group>`**
The resource kind is not available on your cluster. This typically means the CRD has not been installed or the API group is incorrect.

**`get()` returns no results**
- Verify the `namespace` is correct (omit it for cluster-scoped resources).
- Check your `label_selector` and `field_selector` syntax — use `==` for equality (e.g., `field_selector="status.phase==Running"`).
- Ensure the client has permissions to list the resource type.

**`status` property raises `AttributeError`**
Not all resources have a `status.phase` field. Access `instance.status` directly to inspect what status fields are available for your resource type.

## Related Pages

- [Creating and Managing Resources](creating-and-managing-resources.html)
- [Waiting for Resource Conditions and Status](waiting-for-conditions.html)
- [Working with Kubernetes Events](working-with-events.html)
- [Common Resource Patterns](common-patterns.html)
- [Resource and NamespacedResource API](resource-api.html)

---

Source: waiting-for-conditions.md

Let me start by reading the knowledge graph and page manifest, then explore the relevant source code.

Now let me examine the resource-specific wait methods, the `wait_for_condition` more closely, and the constants used:

Now let me look at the `Status` class, `Condition` class, and how `create` integrates with `wait`:

Now let me look at how Status/Condition are exposed on the base Resource class and how `deploy` / `create` uses wait:

Now let me see the `deploy` method more fully and how `wait` is passed through:

Now let me check the `sriov_network_node_state.py` and `virtual_machine_restore.py` for more wait patterns, and also look at the `TimeoutExpiredError` import:

Now let me check the `cdi_config.py` wait method and look at the `wait_for_resource` parameter in the constructor:

Now let me look at how `create` uses `wait`:

Now let me look at more of the `status` property:

Now I have all the information I need. Let me also check the `TimeoutSampler` import to understand the exception:

Let me check if there are any virtual machine import wait patterns with more stop_status details:

Now let me look at the `node_network_state.py` wait patterns:

I now have comprehensive understanding of all the wait methods. Let me compile the documentation.

# Waiting for Resource Conditions and Status

Wait for Kubernetes and OpenShift resources to reach a desired status, condition, or lifecycle state using built-in polling methods with configurable timeouts and early exit on failure.

## Prerequisites

- `openshift-python-wrapper` installed in your project
- A connected client (see [Connecting to Clusters](connecting-to-clusters.html))
- A resource instance to wait on (see [Creating and Managing Resources](creating-and-managing-resources.html))

## Quick Example

```python
from ocp_resources.pod import Pod

pod = Pod(client=client, name="my-pod", namespace="default")
pod.create(wait=True)  # Creates the pod and waits for it to exist

# Wait for the pod to reach "Running" status
pod.wait_for_status(status=Pod.Status.RUNNING)
```

## Waiting for a Resource to Exist

The `wait()` method polls until a resource exists on the cluster. This is useful after creating a resource or when waiting for an external controller to produce one.

```python
from ocp_resources.namespace import Namespace

ns = Namespace(client=client, name="my-namespace")
ns.create()
ns.wait(timeout=120, sleep=2)
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `timeout` | `int` | `240` (4 minutes) | Maximum seconds to wait |
| `sleep` | `int` | `1` | Seconds between polls |

**Raises:** `TimeoutExpiredError` if the resource does not appear before the timeout.

> **Tip:** Pass `wait=True` to `create()` or `deploy()` to combine creation and existence waiting in one call:
> ```python
> pod.create(wait=True)
> # or
> pod.deploy(wait=True)
> ```

## Waiting for Deletion

The `wait_deleted()` method polls until a resource no longer exists:

```python
pod.delete(wait=True, timeout=120)

# Or call wait_deleted() separately:
pod.delete()
success = pod.wait_deleted(timeout=120)
if not success:
    print("Resource still exists after timeout")
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `timeout` | `int` | `240` (4 minutes) | Maximum seconds to wait |

**Returns:** `True` if the resource was deleted, `False` if the timeout was reached.

> **Note:** Unlike most wait methods, `wait_deleted()` returns `False` on timeout rather than raising an exception.

## Waiting for Status (Phase)

The `wait_for_status()` method polls `status.phase` until it matches the expected value:

```python
from ocp_resources.datavolume import DataVolume

dv = DataVolume(client=client, name="my-dv", namespace="default")
dv.wait_for_status(status=DataVolume.Status.SUCCEEDED, timeout=600)
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `status` | `str` | *(required)* | Expected `status.phase` value |
| `timeout` | `int` | `240` (4 minutes) | Maximum seconds to wait |
| `stop_status` | `str \| None` | `Status.FAILED` | Status that aborts the wait immediately |
| `sleep` | `int` | `1` | Seconds between polls |
| `exceptions_dict` | `dict` | Protocol + cluster retry errors | Exceptions to catch and retry |

**Raises:** `TimeoutExpiredError` if the resource does not reach the desired status, or if `stop_status` is encountered.

### Using `stop_status` for early failure

By default, `wait_for_status()` aborts immediately if the resource enters a `"Failed"` phase. Override this to detect a different failure phase:

```python
from ocp_resources.virtual_machine_instance import VirtualMachineInstance

vmi = VirtualMachineInstance(client=client, name="my-vmi", namespace="default")
vmi.wait_for_status(
    status=VirtualMachineInstance.Status.RUNNING,
    stop_status="ErrorUnschedulable",
    timeout=300,
)
```

### Built-in status constants

Every resource inherits status constants from its base class. Resource-specific subclasses extend these with additional values:

```python
# Base status constants (available on all resources)
from ocp_resources.resource import Resource

Resource.Status.SUCCEEDED    # "Succeeded"
Resource.Status.FAILED       # "Failed"
Resource.Status.RUNNING      # "Running"
Resource.Status.PENDING      # "Pending"

# Resource-specific statuses
from ocp_resources.virtual_machine import VirtualMachine

VirtualMachine.Status.MIGRATING     # "Migrating"
VirtualMachine.Status.STOPPED       # "Stopped"
VirtualMachine.Status.STARTING      # "Starting"
VirtualMachine.Status.PAUSED        # "Paused"

from ocp_resources.persistent_volume_claim import PersistentVolumeClaim

PersistentVolumeClaim.Status.BOUND  # "Bound"
```

## Waiting for Conditions

The `wait_for_condition()` method waits for a specific entry in `status.conditions[]` to match a desired type, status, and optionally a reason or message:

```python
from ocp_resources.user_defined_network import UserDefinedNetwork

udn = UserDefinedNetwork(client=client, name="my-net", namespace="default")
udn.wait_for_condition(
    condition=UserDefinedNetwork.Condition.NETWORK_READY,
    status=UserDefinedNetwork.Condition.Status.TRUE,
    timeout=30,
)
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `condition` | `str` | *(required)* | Condition type to match (e.g. `"Ready"`, `"Available"`) |
| `status` | `str` | *(required)* | Expected condition status (`"True"`, `"False"`, `"Unknown"`) |
| `timeout` | `int` | `300` (5 minutes) | Maximum seconds to wait |
| `sleep_time` | `int` | `1` | Seconds between polls |
| `reason` | `str \| None` | `None` | If set, the condition's `reason` field must also match |
| `message` | `str` | `""` | Text that must appear in the condition's `message` field |
| `stop_condition` | `str \| None` | `None` | A condition type that aborts the wait immediately |
| `stop_status` | `str` | `"True"` | The status of the stop condition that triggers early abort |

**Raises:**
- `TimeoutExpiredError` — if the condition is not met before the timeout
- `ConditionError` — if the `stop_condition` is detected before the desired condition

### Matching by reason and message

```python
pod.wait_for_condition(
    condition="Ready",
    status="True",
    reason="PodCompleted",
    message="containers with",
    timeout=120,
)
```

The `reason` must be an exact match. The `message` is checked using substring inclusion — the condition's message field must *contain* the provided string.

### Using `stop_condition` for early abort

Stop conditions let you fail fast when an unrecoverable condition appears:

```python
from ocp_resources.exceptions import ConditionError

try:
    resource.wait_for_condition(
        condition="Ready",
        status="True",
        stop_condition="Failed",
        stop_status="True",
        timeout=300,
    )
except ConditionError as e:
    print(f"Resource hit a failure condition: {e}")
```

> **Note:** The `stop_condition` matching only checks the condition `type` and `status`. The `reason` and `message` parameters are ignored when evaluating stop conditions.

### Built-in condition constants

```python
from ocp_resources.resource import Resource

# Condition types
Resource.Condition.READY         # "Ready"
Resource.Condition.AVAILABLE     # "Available"
Resource.Condition.DEGRADED      # "Degraded"
Resource.Condition.PROGRESSING   # "Progressing"
Resource.Condition.UPGRADEABLE   # "Upgradeable"
Resource.Condition.NETWORK_READY # "NetworkReady"

# Condition statuses
Resource.Condition.Status.TRUE    # "True"
Resource.Condition.Status.FALSE   # "False"
Resource.Condition.Status.UNKNOWN # "Unknown"

# Condition reasons
Resource.Condition.Reason.ALL_REQUIREMENTS_MET  # "AllRequirementsMet"
Resource.Condition.Reason.INSTALL_SUCCEEDED     # "InstallSucceeded"
```

## Waiting with Context Managers

When using a resource as a context manager, set `wait_for_resource=True` to automatically wait for the resource to exist after creation:

```python
from ocp_resources.pod import Pod

with Pod(
    client=client,
    name="my-pod",
    namespace="default",
    wait_for_resource=True,
) as pod:
    # Pod exists when you reach this line
    pod.wait_for_status(status=Pod.Status.RUNNING)
```

See [Creating and Managing Resources](creating-and-managing-resources.html) for more on context manager usage.

## Advanced Usage

### Waiting for any conditions to appear

The `wait_for_conditions()` method waits up to 30 seconds for the resource's `status.conditions` list to be populated (non-empty). This is useful when you need to inspect conditions but aren't sure they've been set yet:

```python
resource.wait_for_conditions()
# Now resource.instance.status.conditions is available
```

### Resource-specific wait methods

Many resource types provide specialized wait methods beyond the base `wait_for_status()` and `wait_for_condition()`. These methods encapsulate domain-specific logic.

#### Deployments

```python
from ocp_resources.deployment import Deployment

deploy = Deployment(client=client, name="my-app", namespace="default")
deploy.wait_for_replicas(deployed=True, timeout=240)   # All replicas ready
deploy.wait_for_replicas(deployed=False, timeout=120)   # Scale to zero complete
```

#### DaemonSets

```python
from ocp_resources.daemonset import DaemonSet

ds = DaemonSet(client=client, name="my-ds", namespace="default")
ds.wait_until_deployed(timeout=240)  # All desired pods are ready
```

#### VirtualMachines (KubeVirt)

```python
from ocp_resources.virtual_machine import VirtualMachine
from ocp_resources.virtual_machine_instance import VirtualMachineInstance

vm = VirtualMachine(client=client, name="my-vm", namespace="default")

# Wait for VM ready status (True = running, None = stopped)
vm.wait_for_ready_status(status=True, timeout=240)

# Wait for a specific status field to become None
vm.wait_for_status_none(status="snapshotInProgress", timeout=240)

# Wait for VMI to be running
vmi = VirtualMachineInstance(client=client, name="my-vm", namespace="default")
vmi.wait_until_running(timeout=240, logs=True)

# Wait for VMI pause state
vmi.wait_for_pause_status(pause=True, timeout=240)
```

See [Working with Virtual Machines (KubeVirt)](working-with-virtual-machines.html) for the full VM lifecycle.

#### DataVolumes

```python
from ocp_resources.datavolume import DataVolume

dv = DataVolume(client=client, name="my-dv", namespace="default")
dv.wait_for_dv_success(
    timeout=600,
    failure_timeout=120,
    pvc_wait_for_bound_timeout=60,
)
```

The `wait_for_dv_success()` method handles the full DataVolume lifecycle: it waits for the status to leave `Pending`, then waits for `Succeeded`, and finally confirms the PVC is `Bound`.

You can pass a `stop_status_func` callback to abort early on custom conditions:

```python
def dv_is_stuck(dv):
    return dv.instance.status.conditions.restartCount > 3

dv.wait_for_dv_success(
    stop_status_func=dv_is_stuck,
    dv=dv,  # passed as keyword argument to the function
)
```

#### VirtualMachineSnapshot / VirtualMachineRestore

```python
from ocp_resources.virtual_machine_snapshot import VirtualMachineSnapshot
from ocp_resources.virtual_machine_restore import VirtualMachineRestore

snapshot = VirtualMachineSnapshot(client=client, name="snap-1", namespace="default")
snapshot.wait_snapshot_done(timeout=240)    # readyToUse + VM snapshotInProgress is None

restore = VirtualMachineRestore(client=client, name="restore-1", namespace="default")
restore.wait_restore_done(timeout=240)     # complete + VM restoreInProgress is None
```

#### MachineSet

```python
from ocp_resources.machine_set import MachineSet

ms = MachineSet(client=client, name="worker-set", namespace="openshift-machine-api")
ms.wait_for_replicas(timeout=300, sleep=1)
```

Returns `True` when ready replicas match desired replicas, `False` on timeout.

#### NodeNetworkConfigurationPolicy

```python
from ocp_resources.node_network_configuration_policy import NodeNetworkConfigurationPolicy

nncp = NodeNetworkConfigurationPolicy(client=client, name="br1-policy")
nncp.wait_for_status_success()  # Uses the resource's own success_timeout
```

Raises `NNCPConfigurationFailed` if the policy fails or finds no matching nodes.

### Timeout constants

The library provides predefined timeout constants you can use with any wait method:

```python
from ocp_resources.utils.constants import (
    TIMEOUT_1SEC,       # 1
    TIMEOUT_5SEC,       # 5
    TIMEOUT_10SEC,      # 10
    TIMEOUT_30SEC,      # 30
    TIMEOUT_1MINUTE,    # 60
    TIMEOUT_2MINUTES,   # 120
    TIMEOUT_4MINUTES,   # 240
    TIMEOUT_10MINUTES,  # 600
)

pod.wait_for_status(status=Pod.Status.RUNNING, timeout=TIMEOUT_10MINUTES)
```

### Custom exception retries

Wait methods handle transient cluster errors automatically (connection resets, etcd leader changes, protocol errors). You can customize which exceptions are retried:

```python
from ocp_resources.utils.constants import (
    PROTOCOL_ERROR_EXCEPTION_DICT,
    DEFAULT_CLUSTER_RETRY_EXCEPTIONS,
)

# Combine default retry exceptions
pod.wait_for_status(
    status=Pod.Status.RUNNING,
    exceptions_dict={
        **PROTOCOL_ERROR_EXCEPTION_DICT,
        **DEFAULT_CLUSTER_RETRY_EXCEPTIONS,
    },
)
```

The default retry exceptions cover:
- `ProtocolError` — network-level failures
- `MaxRetryError`, `ConnectionAbortedError`, `ConnectionResetError` — connection failures
- `InternalServerError` — etcd leader changes, webhook failures, RPC errors
- `ServerTimeoutError` — API server timeouts
- `ForbiddenError` — context deadline exceeded errors

## Troubleshooting

### `TimeoutExpiredError` is raised unexpectedly

- **Increase the timeout.** The default is 240 seconds (4 minutes), which may not be enough for slow operations like DataVolume imports or VM migrations.
- **Check `stop_status`.** `wait_for_status()` defaults `stop_status` to `Status.FAILED`. If your resource legitimately passes through a `"Failed"` phase, set `stop_status` to a value that won't match, or set it to `None`.
- **Inspect the current status.** The error log includes the last observed status. Read it to understand where the resource got stuck.

### `ConditionError` on `wait_for_condition()`

This means the `stop_condition` was matched before the desired condition. The error message includes the full condition details. Review your `stop_condition` and `stop_status` settings.

### Resource exists but `wait_for_status()` times out

The `wait_for_status()` method reads from `status.phase`. Some resources use different status fields (e.g., `status.ready`, `status.readyToUse`, `status.syncStatus`). For those resources, use the resource-specific wait methods documented above, or use `wait_for_condition()` to match on `status.conditions[]` entries instead.

## Related Pages

- [Querying and Watching Resources](querying-resources.html)
- [Creating and Managing Resources](creating-and-managing-resources.html)
- [Working with Virtual Machines (KubeVirt)](working-with-virtual-machines.html)
- [Error Handling and Exception Reference](error-handling.html)
- [Resource and NamespacedResource API](resource-api.html)

---

Source: validating-resources.md

Let me start by reading the exclusions file and pages manifest, then explore the codebase.

Now let me look at how the Resource base class integrates validation:

Now let me check the exceptions module:

Now let me check how `deploy` and context managers interact with validation:

Now I have a comprehensive understanding of the validation feature. Let me write the documentation.

# Validating Resources Against OpenAPI Schemas

Catch configuration mistakes before they reach the Kubernetes API by validating resource definitions against OpenAPI schemas. This saves debugging time and prevents failed deployments caused by typos, missing fields, or incorrect types.

## Prerequisites

- openshift-python-wrapper installed (see [Installing and Creating Your First Resource](quickstart.html))
- A connection to a cluster, or use the fake client for offline validation (see [Testing Without a Cluster Using the Fake Client](testing-without-cluster.html))

## Quick Example

```python
from ocp_resources.pod import Pod

pod = Pod(
    name="nginx-pod",
    namespace="default",
    containers=[{"name": "nginx", "image": "nginx:latest"}],
)

# Validate against the OpenAPI schema — raises ValidationError if invalid
pod.validate()
```

If the resource is valid, `validate()` returns silently. If something is wrong, it raises a `ValidationError` with a clear message pointing to the problematic field.

## Manual Validation

Call `.validate()` on any resource instance to check it against the OpenAPI schema for its kind:

```python
from ocp_resources.exceptions import ValidationError
from ocp_resources.service import Service

service = Service(
    name="nginx-service",
    namespace="default",
    selector={"app": "nginx"},
    ports=[{"port": 80, "targetPort": 80, "protocol": "TCP"}],
)

try:
    service.validate()
    print("Service configuration is valid")
except ValidationError as e:
    print(f"Validation failed: {e}")
```

> **Tip:** Use manual validation in CI pipelines or pre-commit hooks to catch errors before applying resources to a cluster.

## Validating Dictionaries Without Creating Objects

Use the `validate_dict()` class method to validate a raw resource dictionary without instantiating a resource object:

```python
from ocp_resources.deployment import Deployment
from ocp_resources.exceptions import ValidationError

deployment_dict = {
    "apiVersion": "apps/v1",
    "kind": "Deployment",
    "metadata": {"name": "nginx-deployment", "namespace": "default"},
    "spec": {
        "replicas": 3,
        "selector": {"matchLabels": {"app": "nginx"}},
        "template": {
            "metadata": {"labels": {"app": "nginx"}},
            "spec": {
                "containers": [
                    {"name": "nginx", "image": "nginx:1.21", "ports": [{"containerPort": 80}]}
                ],
            },
        },
    },
}

try:
    Deployment.validate_dict(resource_dict=deployment_dict)
    print("Deployment configuration is valid")
except ValidationError as e:
    print(f"Invalid: {e}")
```

This is useful when you have a dictionary from a YAML file or external source and want to validate it before creating the resource.

## Enabling Auto-Validation on Create

Set `schema_validation_enabled=True` when creating a resource instance to automatically validate before every `create()`, `deploy()`, or `update_replace()` call:

```python
from ocp_resources.pod import Pod
from ocp_resources.exceptions import ValidationError

pod = Pod(
    client=client,
    name="auto-validated-pod",
    namespace="default",
    containers=[{"name": "nginx", "image": "nginx:latest"}],
    schema_validation_enabled=True,
)

# Validation runs automatically before the resource is sent to the API
pod.deploy()
```

If validation fails, the resource is **not** created and a `ValidationError` is raised:

```python
invalid_pod = Pod(
    client=client,
    name="bad-pod",
    namespace="default",
    containers=[{"name": "nginx"}],  # Missing required 'image' field
    schema_validation_enabled=True,
)

try:
    invalid_pod.deploy()
except ValidationError as e:
    print(f"Blocked: {e}")
```

> **Note:** Auto-validation is **disabled by default** (`schema_validation_enabled=False`). You must opt in per instance.

### When Auto-Validation Runs

| Operation          | Auto-validates? | Notes                                      |
|--------------------|-----------------|---------------------------------------------|
| `create()` / `deploy()` | ✅ Yes          | Validates the full resource before sending |
| `update_replace()`      | ✅ Yes          | Validates the replacement dictionary       |
| `update()`              | ❌ No           | Sends a partial patch, not a full resource |

Auto-validation also applies when using context managers, since they call `deploy()` internally. See [Creating and Managing Resources](creating-and-managing-resources.html) for details on resource lifecycle methods.

### Toggling Validation at Runtime

You can enable or disable validation on an existing instance:

```python
pod = Pod(
    client=client,
    name="my-pod",
    namespace="default",
    containers=[{"name": "nginx", "image": "nginx:latest"}],
)

# Enable after creation
pod.schema_validation_enabled = True
pod.deploy()  # Will validate first

# Disable for a subsequent operation
pod.schema_validation_enabled = False
```

## Advanced Usage

### Validating Resources with Duplicate API Groups

Some resource kinds (like `DNS`) exist in multiple API groups. The validator uses the resource's `api_group` to select the correct schema automatically:

```python
# These two resources share the kind "DNS" but use different API groups.
# Each is validated against its own schema.
dns_config.validate()       # Uses config.openshift.io schema
dns_operator.validate()     # Uses operator.openshift.io schema
```

See [Common Resource Patterns](common-patterns.html) for more on working with resources that share a kind name.

### Pre-Warming the Schema Cache

The first validation for a resource kind loads and resolves the schema (~25ms). Subsequent validations for the same kind use a cache and are much faster (~2ms). Pre-warm the cache at application startup for critical resource types:

```python
from ocp_resources.pod import Pod
from ocp_resources.deployment import Deployment
from ocp_resources.service import Service
from ocp_resources.exceptions import ValidationError

for resource_cls in [Pod, Deployment, Service]:
    try:
        resource_cls.validate_dict({
            "apiVersion": "v1",
            "kind": resource_cls.kind,
            "metadata": {"name": "warmup"}
        })
    except ValidationError:
        pass  # Expected — we just want to load the schemas
```

### Disabling Validation for Bulk Operations

When creating many resources in a loop, disable auto-validation to avoid repeated checks if you've already validated your templates:

```python
# Validate the template once
Pod.validate_dict(resource_dict=pod_template)

# Then create many instances without per-instance validation
for i in range(100):
    pod = Pod(
        client=client,
        name=f"worker-{i}",
        namespace="default",
        containers=[{"name": "app", "image": "myapp:latest"}],
        schema_validation_enabled=False,  # Skip — already validated
    )
    pod.deploy()
```

## Troubleshooting

### Reading Validation Error Messages

Validation errors include three key pieces of information:

```
Resource validation failed for Pod/my-pod
  Field: spec.containers[0].image
  Error: 123 is not of type 'string'
  Expected type: string
```

- **Resource identifier** — which resource failed (kind and name)
- **Field path** — the JSON path to the problematic field
- **Error details** — what went wrong and what was expected

### Common Validation Error Patterns

| Error message pattern                        | Cause                              | Fix                                              |
|----------------------------------------------|-------------------------------------|--------------------------------------------------|
| `'X' is a required property`                 | Missing a required field           | Add the missing field to your resource definition |
| `'X' is not of type 'Y'`                     | Wrong data type                    | Use the correct type (e.g., `3` not `"3"`)       |
| `'X' does not match pattern`                 | Invalid format                     | Follow Kubernetes naming rules (lowercase, alphanumeric, hyphens) |
| `'X' is not one of [...]`                    | Invalid enum value                 | Use one of the allowed values listed in the error |
| `Additional properties are not allowed`       | Unknown field name                 | Check for typos in field names                   |

### Validation Passes But API Rejects the Resource

OpenAPI schema validation catches structural issues (wrong types, missing fields, invalid formats) but cannot check everything the API server validates. For example:

- Unique name constraints within a namespace
- Resource quota limits
- Admission webhook rules
- Cross-resource references (e.g., a ConfigMap that doesn't exist)

If validation passes but the API still rejects your resource, the issue is likely a server-side constraint.

### No Schema Available for a Resource

If no schema is bundled for a resource kind, `validate()` silently succeeds instead of raising an error. A debug-level log message is emitted:

```
No schema found for MyCustomResource, skipping validation
```

To update bundled schemas, see [class-generator CLI Reference](class-generator-cli.html).

## Related Pages

- [Creating and Managing Resources](creating-and-managing-resources.html)
- [Schema Validation and Code Generation Architecture](schema-validation-internals.html)
- [Error Handling and Exception Reference](error-handling.html)
- [class-generator CLI Reference](class-generator-cli.html)
- [Resource and NamespacedResource API](resource-api.html)

---

Source: managing-resource-lists.md

Let me explore the codebase to understand ResourceList and NamespacedResourceList.

Now let me check the example file more carefully and also look at how the `NamespacedResourceList` actually works in the example vs the source code — I noticed the example passes `namespaces=[ns.name for ns in namespaces]` (a list of strings) but the source code expects a `ResourceList` object. Let me verify.

The example file seems to have an outdated API for `NamespacedResourceList` (passing a list of strings), but the actual source code expects a `ResourceList` object. The tests confirm it passes a `ResourceList`. Let me check if the example is outdated:

The example file is inconsistent with the actual source code. The source code validates that each element in `namespaces` has `ns.kind == "Namespace"`, meaning it expects actual Namespace resource objects (from a ResourceList), not strings. The test confirms this by passing a `ResourceList` fixture. I'll document based on the actual source code, not the potentially outdated example.

Let me also check what methods and properties are available:

Now I have all the information I need. Let me write the documentation.

# Managing Bulk Resources with ResourceList

When you need to create several copies of the same resource or deploy a resource across multiple namespaces, doing it one at a time is tedious and error-prone. `ResourceList` and `NamespacedResourceList` let you create, deploy, and clean up groups of resources in a single operation.

## Prerequisites

- A working cluster connection (see [Connecting to Clusters](connecting-to-clusters.html))
- Familiarity with creating individual resources (see [Creating and Managing Resources](creating-and-managing-resources.html))

## Quick Example

Create three namespaces and deploy them in two lines:

```python
from ocp_resources.namespace import Namespace
from ocp_resources.resource import ResourceList, get_client

client = get_client()

namespaces = ResourceList(resource_class=Namespace, num_resources=3, client=client, name="my-ns")
namespaces.deploy()
# Creates: my-ns-1, my-ns-2, my-ns-3
```

Or use a context manager for automatic cleanup:

```python
with ResourceList(client=client, resource_class=Namespace, name="my-ns", num_resources=3) as namespaces:
    print(namespaces[0].name)  # "my-ns-1"
# All three namespaces are deleted when the block exits
```

## Creating Multiple Copies of a Resource

`ResourceList` creates N copies of a resource type, each with an auto-generated name based on a base name you provide.

**Step 1:** Choose a resource class and specify how many copies you want:

```python
from ocp_resources.namespace import Namespace
from ocp_resources.resource import ResourceList, get_client

client = get_client()
namespaces = ResourceList(
    resource_class=Namespace,
    num_resources=3,
    client=client,
    name="test-ns",
)
```

This creates three `Namespace` objects named `test-ns-1`, `test-ns-2`, and `test-ns-3`. They are not yet deployed to the cluster.

**Step 2:** Deploy all resources:

```python
namespaces.deploy()
```

**Step 3:** Work with individual resources by index or iteration:

```python
# Access by index
first_ns = namespaces[0]
print(first_ns.name)  # "test-ns-1"

# Iterate over all resources
for ns in namespaces:
    print(ns.name)

# Check how many resources are in the list
print(len(namespaces))  # 3
```

**Step 4:** Clean up all resources when done:

```python
namespaces.clean_up()
```

> **Note:** `clean_up()` deletes resources in reverse order. This ensures dependent resources are removed before the resources they depend on.

## Deploying One Resource Per Namespace

`NamespacedResourceList` creates one instance of a namespaced resource in each namespace from a `ResourceList`.

```python
from ocp_resources.namespace import Namespace
from ocp_resources.pod import Pod
from ocp_resources.resource import NamespacedResourceList, ResourceList, get_client

client = get_client()

# First, create the namespaces
namespaces = ResourceList(resource_class=Namespace, num_resources=3, client=client, name="app-ns")
namespaces.deploy()

# Then create one pod in each namespace
pods = NamespacedResourceList(
    client=client,
    resource_class=Pod,
    namespaces=namespaces,
    name="web-server",
    containers=[{"name": "nginx", "image": "nginx:latest"}],
)
pods.deploy()
```

This creates three pods, all named `web-server`, one in each namespace (`app-ns-1`, `app-ns-2`, `app-ns-3`).

```python
for pod in pods:
    print(f"{pod.name} in {pod.namespace}")
# web-server in app-ns-1
# web-server in app-ns-2
# web-server in app-ns-3
```

> **Warning:** The `namespaces` parameter must be a `ResourceList` containing only Namespace resources. Passing any other resource type raises a `TypeError`.

## Passing Extra Resource Parameters

Any additional keyword arguments you pass are forwarded to each resource's constructor. This lets you configure resources beyond just the name:

```python
from ocp_resources.role import Role
from ocp_resources.resource import NamespacedResourceList

roles = NamespacedResourceList(
    client=client,
    resource_class=Role,
    namespaces=namespaces,
    name="viewer-role",
    rules=[
        {
            "apiGroups": [""],
            "resources": ["pods"],
            "verbs": ["get", "list", "watch"],
        }
    ],
)
roles.deploy()
```

## Advanced Usage

### Context Managers for Automatic Lifecycle

Both `ResourceList` and `NamespacedResourceList` support context managers. Resources are deployed on entry and cleaned up on exit — ideal for tests:

```python
with ResourceList(client=client, resource_class=Namespace, name="test-ns", num_resources=3) as namespaces:
    with NamespacedResourceList(
        client=client,
        resource_class=Pod,
        namespaces=namespaces,
        name="test-pod",
        containers=[{"name": "app", "image": "busybox"}],
    ) as pods:
        # All namespaces and pods are deployed
        assert len(pods) == 3
    # Pods are cleaned up here
# Namespaces are cleaned up here
```

> **Tip:** Nest the context managers so that namespaced resources are cleaned up before their namespaces are deleted.

### Waiting During Deploy and Cleanup

Pass `wait=True` to `deploy()` to wait for each resource to reach a ready state. By default, `clean_up()` already waits for deletion to complete.

```python
namespaces = ResourceList(resource_class=Namespace, num_resources=2, client=client, name="ns")
namespaces.deploy(wait=True)

# Later...
namespaces.clean_up(wait=True)   # wait=True is the default
namespaces.clean_up(wait=False)  # Skip waiting for deletion
```

For more on waiting for specific conditions, see [Waiting for Resource Conditions and Status](waiting-for-conditions.html).

### Comparison: ResourceList vs NamespacedResourceList

| Feature | `ResourceList` | `NamespacedResourceList` |
|---|---|---|
| **Purpose** | Create N copies of any resource | Create one resource per namespace |
| **Naming** | Auto-generates `{name}-1`, `{name}-2`, … | Same name in each namespace |
| **Required params** | `resource_class`, `num_resources`, `name` | `resource_class`, `namespaces`, `name` |
| **Use case** | Multiple namespaces, nodes, cluster-scoped resources | Pods, services, roles — anything namespace-scoped |
| **Context manager** | ✅ | ✅ |

## Troubleshooting

**TypeError: "All the resources in namespaces should be namespaces"**

You passed a `ResourceList` that contains non-Namespace resources to `NamespacedResourceList`. Make sure the `namespaces` parameter is a `ResourceList` created with `resource_class=Namespace`.

**Resources not appearing on the cluster**

Creating a `ResourceList` or `NamespacedResourceList` only instantiates the Python objects — it does not deploy them. Call `.deploy()` or use a context manager (`with ... as`) to actually create the resources on the cluster.

**Cleanup order issues**

Always clean up namespaced resources before their namespaces. When using context managers, nest them so the inner block (namespaced resources) exits first. When calling `clean_up()` manually, clean up dependent resources before their parents.

## Related Pages

- [Creating and Managing Resources](creating-and-managing-resources.html)
- [Waiting for Resource Conditions and Status](waiting-for-conditions.html)
- [Testing Without a Cluster Using the Fake Client](testing-without-cluster.html)
- [Common Resource Patterns](common-patterns.html)
- [Resource and NamespacedResource API](resource-api.html)

---

Source: editing-resources-temporarily.md

Let me start by reading the knowledge graph and understanding the repository structure.

Now let me find the ResourceEditor source code.

Now let me look at how ResourceEditor is used in the NNCP file and any tests:

Let me look at the import section and any related constants:

Now let me look at the `update` and `update_replace` methods on the Resource class to understand the two actions:

Now let me look at the imports to verify how ResourceEditor is exported:

Now let me check for any additional examples or documentation:

Let me look at the full ResourceEditor code once more to make sure I've captured everything, and also check the `_apply_patches` method details and what `action` values are supported:

Now I have a complete picture of the ResourceEditor class. Let me write the documentation.

# Editing Resources Temporarily with ResourceEditor

Temporarily modify Kubernetes and OpenShift resources during tests and have their original values automatically restored when you're done. This is essential for test isolation—ensuring that changes made during a test don't leak into other tests or persist in the cluster.

## Prerequisites

- `openshift-python-wrapper` installed in your project (see [Installing and Creating Your First Resource](quickstart.html))
- A cluster connection established with a privileged client (see [Connecting to Clusters](connecting-to-clusters.html))
- One or more existing resources on the cluster that you want to temporarily modify

> **Warning:** The client used to retrieve resources must have sufficient privileges. Do not use an unprivileged user client with `ResourceEditor`.

## Quick Example

```python
from ocp_resources.resource import ResourceEditor
from ocp_resources.node import Node

node = Node(name="worker-0")

with ResourceEditor(
    patches={node: {"metadata": {"labels": {"test-label": "true"}}}}
):
    # The node now has the label "test-label": "true"
    # Run your test logic here...
    pass

# The label is automatically removed when the block exits
```

That's it—when the `with` block ends (even if an exception occurs), the original resource state is restored.

## Step-by-Step: Using ResourceEditor as a Context Manager

### 1. Import and identify your target resource

```python
from ocp_resources.resource import ResourceEditor
from ocp_resources.config_map import ConfigMap

cm = ConfigMap(name="my-config", namespace="my-namespace")
```

### 2. Define your patch

Patches are dictionaries that mirror the resource's YAML structure. Only include the fields you want to change:

```python
patch = {"data": {"feature_flag": "enabled", "log_level": "debug"}}
```

### 3. Wrap your test logic in a `with` block

```python
with ResourceEditor(patches={cm: patch}):
    # ConfigMap now has updated data fields
    assert cm.instance.data.feature_flag == "enabled"
    # Run tests that depend on the modified config...
```

### 4. Original values are restored automatically

When the `with` block exits, the original `data` values are written back to the resource. Fields that didn't exist before the patch are removed (set to `None` in the restore payload, which tells the API to delete them).

## Patching Multiple Resources at Once

Pass multiple resource-patch pairs in a single `ResourceEditor`:

```python
from ocp_resources.resource import ResourceEditor
from ocp_resources.namespace import Namespace
from ocp_resources.config_map import ConfigMap

ns = Namespace(name="test-ns")
cm = ConfigMap(name="app-config", namespace="test-ns")

patches = {
    ns: {"metadata": {"labels": {"env": "test"}}},
    cm: {"data": {"database_url": "postgres://test-db:5432"}},
}

with ResourceEditor(patches=patches):
    # Both resources are patched; run tests here
    pass
# Both resources are restored to their original state
```

## Using update() and restore() Without a Context Manager

If you need finer control over when patches are applied and reverted—for example, in `setup`/`teardown` fixtures—use the `update()` and `restore()` methods directly:

```python
from ocp_resources.resource import ResourceEditor
from ocp_resources.config_map import ConfigMap

cm = ConfigMap(name="app-config", namespace="default")

editor = ResourceEditor(
    patches={cm: {"data": {"mode": "maintenance"}}}
)

# Apply the patch and create backups
editor.update(backup_resources=True)

# ... run tests ...

# Restore original values
editor.restore()
```

> **Note:** When calling `update()` manually, pass `backup_resources=True` to ensure original values are captured. Without this flag, no backup is created and `restore()` will have nothing to revert.

## Advanced Usage

### Replace Instead of Patch

By default, `ResourceEditor` uses the `"update"` action, which sends a merge-patch (`application/merge-patch+json`). This merges your changes into the existing resource. If you need to *replace* the entire resource (for example, to remove fields that merge-patch can't delete), use `action="replace"`:

```python
from ocp_resources.resource import ResourceEditor

with ResourceEditor(
    patches={my_resource: {"spec": {"replicas": 3}}},
    action="replace",
):
    # The resource is fully replaced, not merged
    pass
```

| Action | Method Used | Behavior |
|---|---|---|
| `"update"` (default) | Merge patch | Merges patch into existing resource; cannot remove fields by omission |
| `"replace"` | Full replacement | Replaces the entire resource; fields not in the patch are removed |

> **Warning:** With `action="replace"`, the patch must include all required fields for the resource, not just the fields you want to change. The replacement payload automatically includes `metadata.name`, `metadata.namespace`, `metadata.resourceVersion`, `kind`, and `apiVersion`.

### Providing Your Own Backups

If you already know what the restore state should be (or need custom restore logic), pass `user_backups` to skip the automatic backup calculation:

```python
from ocp_resources.resource import ResourceEditor

custom_backup = {my_resource: {"spec": {"replicas": 1}}}

with ResourceEditor(
    patches={my_resource: {"spec": {"replicas": 5}}},
    user_backups=custom_backup,
):
    # Resource scaled to 5 replicas
    pass
# Restored to 1 replica (your custom backup), regardless of what the original value was
```

### Inspecting Backups

Access the automatically generated backup data through the `backups` property:

```python
editor = ResourceEditor(
    patches={my_resource: {"metadata": {"labels": {"env": "staging"}}}}
)
editor.update(backup_resources=True)

# See what was backed up
print(editor.backups)
# {<Resource>: {'metadata': {'labels': {'env': 'production'}}}}

editor.restore()
```

### No-Op Detection

`ResourceEditor` automatically detects when a patch produces no actual changes. If the patch values already match the resource's current state, the resource is skipped entirely and a warning is logged:

```
ResourceEdit: no diff found in patch for my-resource -- skipping
```

This prevents unnecessary API calls and avoids triggering watch events for no-op changes.

### Automatic Retry on Conflicts

All patch and restore operations are wrapped with automatic retry logic. If a `ConflictError` occurs (for example, due to a concurrent update changing the `resourceVersion`), the operation is retried automatically with a 5-second sleep interval and a 30-second timeout.

## API Reference

### Constructor

```python
ResourceEditor(
    patches: dict,
    action: str = "update",
    user_backups: dict | None = None,
)
```

| Parameter | Type | Default | Description |
|---|---|---|---|
| `patches` | `dict` | *(required)* | Mapping of resource objects to patch dicts: `{resource: {yaml_patch}}` |
| `action` | `str` | `"update"` | Either `"update"` (merge patch) or `"replace"` (full replacement) |
| `user_backups` | `dict \| None` | `None` | Custom backup dicts to use for restore instead of auto-generated ones |

### Properties

| Property | Return Type | Description |
|---|---|---|
| `backups` | `dict` | The backup data captured for each patched resource: `{resource: backup_dict}` |
| `patches` | `dict` | The patches dict provided in the constructor |

### Methods

| Method | Parameters | Description |
|---|---|---|
| `update(backup_resources=False)` | `backup_resources: bool` | Apply patches. Set `backup_resources=True` to capture original values for later restore. |
| `restore()` | — | Re-apply the stored backup dicts to revert all patched resources. |

### Context Manager Protocol

| Method | Behavior |
|---|---|
| `__enter__()` | Calls `update(backup_resources=True)` and returns the `ResourceEditor` instance |
| `__exit__()` | Calls `restore()` to revert all changes |

## Troubleshooting

| Problem | Cause | Solution |
|---|---|---|
| Restore doesn't revert changes | Called `update()` without `backup_resources=True` | Always pass `backup_resources=True` when calling `update()` manually, or use the context manager (`with` block) which does this automatically |
| `NotFoundError` during backup | Resource cannot be found by name (known with some CRDs like `ServiceMonitor`) | `ResourceEditor` handles this automatically by falling back to a field-selector query |
| `ConflictError` during patch | Another process modified the resource concurrently | Automatic retries handle this; if it persists beyond 30 seconds, check for controllers continuously modifying the resource |
| Patch has no effect | The resource already has the values in your patch | This is expected—`ResourceEditor` logs a warning and skips the resource. Check that you're patching the correct fields. |
| `replace` action fails | Patch is missing required fields | When using `action="replace"`, provide all required spec fields since the entire resource is replaced |

> **Tip:** For creating and cleaning up resources (rather than editing existing ones), see [Creating and Managing Resources](creating-and-managing-resources.html). For managing groups of similar resources, see [Managing Bulk Resources with ResourceList](managing-resource-lists.html).

## Related Pages

- [Creating and Managing Resources](creating-and-managing-resources.html)
- [Resource and NamespacedResource API](resource-api.html)
- [Common Resource Patterns](common-patterns.html)
- [Managing Bulk Resources with ResourceList](managing-resource-lists.html)
- [Testing Without a Cluster Using the Fake Client](testing-without-cluster.html)

---

Source: working-with-virtual-machines.md

Let me start by exploring the knowledge graph and understanding the repository structure.

Now let me look at the remaining VM-related resources and check the `RunStrategy` and `Status` constants, as well as the `data_volume.py`:

Now let me check how the `api_request` method works in the base `Resource` class:

Now let me look at the `data_volume.py` to check any VM-related interactions:

Now let me check the `RunStrategy` constants in VirtualMachineInstance:

Now let me look at the `Status` class in the base `Resource` for context and the VMI's `RunStrategy`:

Let me find the Status class definition:

Now let me check the `ApiGroup` constants for KubeVirt:

Now let me check for any tests related to VMs:

Now let me check the `deploy` and `clean_up` method signatures:

Now I have a thorough understanding of the VM-related API. Let me write the documentation:

# Working with Virtual Machines (KubeVirt)

Manage the full lifecycle of KubeVirt virtual machines on OpenShift — create VMs, start and stop them, live-migrate between nodes, take snapshots, and interact with their running instances — all from Python.

## Prerequisites

- `openshift-python-wrapper` installed (see [Installing and Creating Your First Resource](quickstart.html))
- A cluster connection configured (see [Connecting to Clusters](connecting-to-clusters.html))
- KubeVirt / OpenShift Virtualization installed on your cluster

## Quick Example

```python
from ocp_resources.virtual_machine import VirtualMachine

vm = VirtualMachine(
    name="my-vm",
    namespace="my-namespace",
    body={
        "spec": {
            "domain": {
                "devices": {"disks": [{"disk": {"bus": "virtio"}, "name": "rootdisk"}]},
                "resources": {"requests": {"memory": "1Gi"}},
            },
            "volumes": [{"name": "rootdisk", "containerDisk": {"image": "quay.io/containerdisks/fedora"}}],
        }
    },
)

with vm:
    vm.start(wait=True)
    print(f"VM ready: {vm.ready}")       # True
    print(f"Status: {vm.printable_status}")  # "Running"
    vm.stop(wait=True)
# VM is automatically cleaned up when the context manager exits
```

## Creating a VirtualMachine

```python
from ocp_resources.virtual_machine import VirtualMachine

vm = VirtualMachine(
    name="my-vm",
    namespace="my-namespace",
    body={
        "spec": {
            "domain": {
                "devices": {"disks": [{"disk": {"bus": "virtio"}, "name": "rootdisk"}]},
                "resources": {"requests": {"memory": "2Gi"}},
            },
            "volumes": [
                {
                    "name": "rootdisk",
                    "containerDisk": {"image": "quay.io/containerdisks/fedora"},
                }
            ],
        }
    },
)
vm.deploy()
```

The `body` parameter accepts a dictionary that maps to the KubeVirt VM `spec.template` structure. If you omit `body`, a minimal skeleton (`{"template": {"spec": {}}}`) is created automatically.

You can also create a VM from a YAML file:

```python
vm = VirtualMachine(
    name="my-vm",
    namespace="my-namespace",
    yaml_file="vm-definition.yaml",
)
vm.deploy()
```

### Constructor Parameters

| Parameter | Type | Default | Description |
|---|---|---|---|
| `name` | `str` | `None` | Name of the VirtualMachine resource |
| `namespace` | `str` | `None` | Target namespace |
| `client` | `DynamicClient` | `None` | Kubernetes client; auto-resolved if omitted |
| `body` | `dict` | `None` | VM spec body dictionary |
| `teardown` | `bool` | `True` | Whether to delete the resource on cleanup |
| `yaml_file` | `str` | `None` | Path to a YAML file defining the VM |
| `delete_timeout` | `int` | `240` (seconds) | Timeout for delete operations |

> **Tip:** Use the context manager (`with vm:`) to ensure automatic cleanup. See [Creating and Managing Resources](creating-and-managing-resources.html) for details on `deploy()`, `clean_up()`, and context managers.

## Starting, Stopping, and Restarting

### Start a VM

```python
# Fire-and-forget
vm.start()

# Wait until the VM is ready
vm.start(wait=True)

# Custom timeout (seconds)
vm.start(timeout=600, wait=True)
```

### Stop a VM

```python
# Fire-and-forget
vm.stop()

# Wait for the VM to stop and the VMI to be deleted
vm.stop(wait=True)

# Custom timeouts
vm.stop(timeout=300, vmi_delete_timeout=300, wait=True)
```

### Restart a VM

```python
vm.restart(wait=True)
```

When `wait=True`, `restart()` waits for the old virt-launcher pod to be deleted and the new VMI to reach the `Running` state.

## Checking VM Status

```python
# Boolean ready status: True if running, None if stopped
vm.ready  # True

# Human-readable status string
vm.printable_status  # "Running", "Stopped", "Starting", "Migrating", etc.

# Wait for ready status
vm.wait_for_ready_status(status=True, timeout=300)   # Wait for running
vm.wait_for_ready_status(status=None, timeout=300)    # Wait for stopped

# Wait for a specific status field to clear
vm.wait_for_status_none(status="snapshotInProgress")
```

### Run Strategy Constants

Use built-in constants for the `runStrategy` field:

```python
VirtualMachine.RunStrategy.ALWAYS            # "Always"
VirtualMachine.RunStrategy.HALTED            # "Halted"
VirtualMachine.RunStrategy.MANUAL            # "Manual"
VirtualMachine.RunStrategy.RERUNONFAILURE    # "RerunOnFailure"
```

### Status Constants

```python
VirtualMachine.Status.STARTING       # "Starting"
VirtualMachine.Status.RUNNING        # "Running"  (inherited)
VirtualMachine.Status.STOPPED        # "Stopped"
VirtualMachine.Status.STOPPING       # "Stopping"
VirtualMachine.Status.MIGRATING      # "Migrating"
VirtualMachine.Status.PAUSED         # "Paused"
VirtualMachine.Status.PROVISIONING   # "Provisioning"
VirtualMachine.Status.ERROR_UNSCHEDULABLE  # "ErrorUnschedulable"
VirtualMachine.Status.DATAVOLUME_ERROR     # "DataVolumeError"
```

## Working with VirtualMachineInstances

Every running VM has an associated `VirtualMachineInstance` (VMI). Access it through the VM or create one directly.

### Accessing the VMI from a VM

```python
vmi = vm.vmi  # Returns a VirtualMachineInstance object
```

### Creating a Standalone VMI

```python
from ocp_resources.virtual_machine_instance import VirtualMachineInstance

vmi = VirtualMachineInstance(
    name="my-vmi",
    namespace="my-namespace",
    domain={
        "devices": {"disks": [{"disk": {"bus": "virtio"}, "name": "rootdisk"}]},
        "resources": {"requests": {"memory": "1Gi"}},
    },
    volumes=[
        {"name": "rootdisk", "containerDisk": {"image": "quay.io/containerdisks/fedora"}}
    ],
)
vmi.deploy()
```

> **Note:** The `domain` parameter is **required** when creating a VMI programmatically. Omitting it raises `MissingRequiredArgumentError`.

### VMI Constructor Parameters

| Parameter | Type | Required | Description |
|---|---|---|---|
| `domain` | `dict` | **Yes** | Domain spec (CPU, memory, devices) |
| `volumes` | `list` | No | Volumes to mount |
| `networks` | `list` | No | Network interfaces |
| `affinity` | `dict` | No | Scheduling affinity rules |
| `node_selector` | `dict` | No | Node label selector |
| `tolerations` | `list` | No | Toleration rules |
| `eviction_strategy` | `str` | No | `"LiveMigrate"`, `"LiveMigrateIfPossible"`, `"None"`, or `"External"` |
| `termination_grace_period_seconds` | `int` | No | Seconds to wait before force-killing |
| `hostname` | `str` | No | VM hostname |
| `priority_class_name` | `str` | No | Pod priority class |
| `scheduler_name` | `str` | No | Custom scheduler name |
| `start_strategy` | `str` | No | Set to `"Paused"` to start paused |
| `access_credentials` | `list` | No | Public keys to inject |
| `topology_spread_constraints` | `list` | No | Topology spread rules |

### Waiting for a VMI to Run

```python
vmi.wait_until_running(timeout=300)
```

If the VMI fails to start, `wait_until_running` raises `TimeoutExpiredError` and logs the virt-launcher pod status and logs for debugging. Set `logs=False` to suppress this:

```python
vmi.wait_until_running(timeout=300, logs=False)
```

You can also specify a `stop_status` to abort early if the VMI enters a failure state:

```python
vmi.wait_until_running(timeout=300, stop_status="Failed")
```

### Pausing and Unpausing

```python
vmi.pause(wait=True)    # Pause the VMI
vmi.unpause(wait=True)  # Resume the VMI
```

### Resetting a VMI

```python
vmi.reset()  # Hard-reset the VMI (like pressing the reset button)
```

### Getting the VMI Node

```python
node = vmi.get_node()
print(node.name)
```

### Network Interfaces

```python
# All interfaces from the VMI status
interfaces = vmi.interfaces

# Get IP address for a specific interface
ip = vmi.interface_ip("eth0")
```

### Guest Agent Information

When the QEMU guest agent is installed in the VM:

```python
vmi.guest_os_info       # OS information
vmi.guest_fs_info       # Filesystem information
vmi.guest_user_info     # Logged-in user information
vmi.os_version          # OS version string
```

> **Note:** If no guest agent is installed, `os_version` returns an empty dict and logs a warning.

### Accessing the Virt-Launcher Pod

```python
pod = vmi.get_virt_launcher_pod()
print(pod.name)
print(pod.status)

# Access logs from the compute container
logs = pod.log(container="compute")
```

For clusters where a privileged client is required:

```python
pod = vmi.get_virt_launcher_pod(privileged_client=admin_client)
```

### Accessing the Virt-Handler Pod

```python
handler_pod = vmi.get_virt_handler_pod(privileged_client=admin_client)
```

### Executing Virsh Commands

Run virsh commands inside the virt-launcher pod:

```python
# Get the XML definition
xml_output = vmi.get_xml()

# Get the XML as a parsed Python dict
xml_dict = vmi.get_xml_dict()

# Get domain memory stats
memstats = vmi.get_dommemstat()

# Run any arbitrary virsh command
output = vmi.execute_virsh_command(command="dominfo")
```

### Checking Root Status

```python
pod = vmi.get_virt_launcher_pod()

# Check if the virt-launcher pod runs as root
VirtualMachineInstance.is_pod_root(pod=pod)  # True or False

# Get the pod's runAsUser UID
VirtualMachineInstance.get_pod_user_uid(pod=pod)  # int or None
```

## Live Migration

Trigger a live migration by creating a `VirtualMachineInstanceMigration`:

```python
from ocp_resources.virtual_machine_instance_migration import VirtualMachineInstanceMigration

migration = VirtualMachineInstanceMigration(
    name="migrate-my-vm",
    namespace="my-namespace",
    vmi_name="my-vm",
)
migration.deploy()
```

Monitor migration by checking the VMI's status. Once migration completes, the VMI runs on the target node. See [Waiting for Resource Conditions and Status](waiting-for-conditions.html) for condition-based waiting.

### Migration Resource Quotas

Control resource allocation for migrations:

```python
from ocp_resources.virtual_machine_migration_resource_quota import VirtualMachineMigrationResourceQuota

quota = VirtualMachineMigrationResourceQuota(
    name="migration-quota",
    namespace="my-namespace",
    requests_cpu="2",
    requests_memory="4Gi",
    limits_cpu="4",
    limits_memory="8Gi",
)
quota.deploy()
```

## Snapshots and Restores

### Taking a Snapshot

```python
from ocp_resources.virtual_machine_snapshot import VirtualMachineSnapshot

snapshot = VirtualMachineSnapshot(
    name="my-vm-snapshot",
    namespace="my-namespace",
    vm_name="my-vm",
)
snapshot.deploy()

# Wait for the snapshot to be ready
snapshot.wait_ready_to_use(timeout=300)

# Wait for both snapshot readiness AND the VM's snapshotInProgress to clear
snapshot.wait_snapshot_done(timeout=300)
```

### Restoring from a Snapshot

```python
from ocp_resources.virtual_machine_restore import VirtualMachineRestore

restore = VirtualMachineRestore(
    name="my-vm-restore",
    namespace="my-namespace",
    vm_name="my-vm",
    snapshot_name="my-vm-snapshot",
)
restore.deploy()

# Wait for restore to complete
restore.wait_restore_done(timeout=300)
```

The `wait_restore_done()` method checks both the restore's `complete` status and that the VM's `restoreInProgress` field is cleared.

| Parameter | Type | Description |
|---|---|---|
| `vm_name` | `str` | Target VirtualMachine name |
| `snapshot_name` | `str` | Source VirtualMachineSnapshot name |
| `volume_restore_policy` | `str` | Optional policy for volume restoration |

## Cloning VMs

```python
from ocp_resources.virtual_machine_clone import VirtualMachineClone

clone = VirtualMachineClone(
    name="clone-my-vm",
    namespace="my-namespace",
    source_name="my-vm",
    target_name="my-vm-clone",
)
clone.deploy()
```

> **Note:** `source_name` is **required**. If omitted, `MissingRequiredArgumentError` is raised.

### Clone Parameters

| Parameter | Type | Default | Description |
|---|---|---|---|
| `source_name` | `str` | **Required** | Name of the source VM |
| `source_kind` | `str` | `"VirtualMachine"` | Kind of the source resource |
| `target_name` | `str` | Auto-generated | Name for the cloned VM |
| `label_filters` | `list` | `None` | Labels to copy, e.g. `["*", "!someKey/*"]` |
| `annotation_filters` | `list` | `None` | Annotations to copy |
| `new_mac_addresses` | `dict` | `None` | Override MACs: `{"nic0": "00:11:22:33:44:55"}` |
| `new_smbios_serial` | `str` | `None` | Override SMBIOS serial number |
| `volume_name_policy` | `str` | `None` | Volume naming policy for the clone |

## Exporting VMs

```python
from ocp_resources.virtual_machine_export import VirtualMachineExport

export = VirtualMachineExport(
    name="export-my-vm",
    namespace="my-namespace",
    source={"apiGroup": "kubevirt.io", "kind": "VirtualMachine", "name": "my-vm"},
    token_secret_ref="my-export-token",
    ttl_duration="1h",
)
export.deploy()
```

> **Note:** The `source` parameter is **required**. If omitted, `MissingRequiredArgumentError` is raised.

## Advanced Usage

### Instance Types and Preferences

KubeVirt instance types define reusable VM sizing profiles. Preferences define non-sizing defaults (clock, firmware, devices).

#### Namespaced Instance Type

```python
from ocp_resources.virtual_machine_instancetype import VirtualMachineInstancetype

instancetype = VirtualMachineInstancetype(
    name="small",
    namespace="my-namespace",
    cpu={"guest": 2},
    memory={"guest": "4Gi"},
)
instancetype.deploy()
```

Both `cpu` and `memory` are **required**. Omitting either raises `MissingRequiredArgumentError`.

#### Cluster-Scoped Instance Type

```python
from ocp_resources.virtual_machine_cluster_instancetype import VirtualMachineClusterInstancetype

cluster_instancetype = VirtualMachineClusterInstancetype(
    name="large",
    cpu={"guest": 8},
    memory={"guest": "16Gi"},
    gpus=[{"deviceName": "nvidia.com/A100", "name": "gpu0"}],
)
cluster_instancetype.deploy()
```

#### Namespaced Preference

```python
from ocp_resources.virtual_machine_preference import VirtualMachinePreference

preference = VirtualMachinePreference(
    name="linux-pref",
    namespace="my-namespace",
    cpu={"preferredCPUTopology": "preferSockets"},
    firmware={"preferredUseEfi": True},
)
preference.deploy()
```

#### Cluster-Scoped Preference

```python
from ocp_resources.virtual_machine_cluster_preference import VirtualMachineClusterPreference

cluster_preference = VirtualMachineClusterPreference(
    name="windows-pref",
    clock={"preferredTimer": {"hpet": {"present": False}}},
    features={"preferredHyperv": {"spinlocks": {"spinlocks": 8191}}},
)
cluster_preference.deploy()
```

### VMI Replica Sets

Scale out VMIs horizontally:

```python
from ocp_resources.virtual_machine_instance_replica_set import VirtualMachineInstanceReplicaSet

replica_set = VirtualMachineInstanceReplicaSet(
    name="my-vmi-rs",
    namespace="my-namespace",
    replicas=3,
    selector={"matchLabels": {"app": "my-vmi"}},
    template={
        "metadata": {"labels": {"app": "my-vmi"}},
        "spec": {
            "domain": {
                "devices": {},
                "resources": {"requests": {"memory": "1Gi"}},
            }
        },
    },
)
replica_set.deploy()
```

Both `selector` and `template` are **required**.

### Storage Migration

Migrate VM storage between storage classes:

```python
from ocp_resources.virtual_machine_storage_migration_plan import VirtualMachineStorageMigrationPlan
from ocp_resources.virtual_machine_storage_migration import VirtualMachineStorageMigration

# Create a migration plan
plan = VirtualMachineStorageMigrationPlan(
    name="migrate-storage",
    namespace="my-namespace",
    virtual_machines=[
        {"name": "my-vm", "volumes": [{"name": "rootdisk", "target": {"storageClassName": "fast-ssd"}}]}
    ],
    retention_policy="deleteSource",
)
plan.deploy()

# Execute the migration
migration = VirtualMachineStorageMigration(
    name="migrate-storage-exec",
    namespace="my-namespace",
    virtual_machine_storage_migration_plan_ref={"name": "migrate-storage", "namespace": "my-namespace"},
)
migration.deploy()
```

### VM Templates

```python
from ocp_resources.virtual_machine_template import VirtualMachineTemplate

template = VirtualMachineTemplate(
    name="fedora-template",
    namespace="my-namespace",
    virtual_machine={
        "spec": {
            "template": {
                "spec": {
                    "domain": {
                        "devices": {},
                        "resources": {"requests": {"memory": "2Gi"}},
                    }
                }
            }
        }
    },
    parameters=[
        {"name": "VM_NAME", "description": "Name of the VM", "required": True}
    ],
    message="Use this template to create Fedora VMs.",
)
template.deploy()
```

### Listing VMs Across Namespaces

```python
# List all VMs in a namespace
for vm in VirtualMachine.get(namespace="my-namespace"):
    print(f"{vm.name}: {vm.instance.get('status', {}).get('printableStatus')}")

# List with label selectors
for vm in VirtualMachine.get(
    namespace="my-namespace",
    label_selector="app=webserver",
):
    print(vm.name)
```

See [Querying and Watching Resources](querying-resources.html) for more query options.

### Getting VM Network Interfaces

```python
interfaces = vm.get_interfaces()
for iface in interfaces:
    print(f"Name: {iface['name']}, Model: {iface.get('model', 'default')}")
```

## Troubleshooting

### VM Stuck in "Starting" Status

Check the VMI's virt-launcher pod for scheduling or image-pull issues:

```python
vmi = vm.vmi
try:
    pod = vmi.get_virt_launcher_pod()
    print(f"Pod status: {pod.status}")
    print(pod.log(container="compute"))
except ResourceNotFoundError:
    print("Virt-launcher pod not found — VM may not have been scheduled yet")
```

### TimeoutExpiredError on start/stop

Increase the timeout parameter:

```python
vm.start(timeout=600, wait=True)  # 10 minutes instead of default 4
```

### MissingRequiredArgumentError

This is raised when a required constructor parameter is missing. Common cases:

| Resource | Required Parameters |
|---|---|
| `VirtualMachineInstance` | `domain` |
| `VirtualMachineInstancetype` | `cpu`, `memory` |
| `VirtualMachineClone` | `source_name` |
| `VirtualMachineExport` | `source` |
| `VirtualMachineInstanceReplicaSet` | `selector`, `template` |
| `VirtualMachineStorageMigration` | `virtual_machine_storage_migration_plan_ref` |
| `VirtualMachineTemplate` | `virtual_machine` |

### Guest Agent Not Reporting

If `vmi.os_version` returns an empty dict, the QEMU guest agent is not installed or not running inside the VM. Install and start `qemu-guest-agent` in the guest OS.

## Related Pages

- [Creating and Managing Resources](creating-and-managing-resources.html)
- [Waiting for Resource Conditions and Status](waiting-for-conditions.html)
- [Querying and Watching Resources](querying-resources.html)
- [Common Resource Patterns](common-patterns.html)
- [Executing Commands in Pods and Retrieving Logs](pod-execution-and-logs.html)

---

Source: testing-without-cluster.md

Let me start by exploring the repository structure and the knowledge graph.

Now let me look at the test files for the fake client:

Let me look at a few more test files to understand patterns used:

Now let me look at how `get_client` works with `fake=True` and how resource classes use the client:

Now let me look at a few more test examples to understand patterns:

Now let me check how resources use the fake client and how `deploy`, `clean_up`, `update` work:

Let me look at additional test files for more patterns - ConfigMap, events, and the configmap resource:

Now let me check how watch and events are used, and how to use the lower-level API:

Now let me check the `events` and `watch` implementation in the Resource class:

Now let me look at a few more resource test files and the low-level API usage:

Let me check for import paths and the `ocp_resources.resource` module to understand the public API:

Let me check how the `wait_for_condition` and other resource methods work with the fake client:

Now I have enough information to write the documentation page. Let me check a couple more things:

Now I have all the information I need. Let me write the documentation page.

# Testing Without a Cluster Using the Fake Client

Write unit tests that create, read, update, and delete Kubernetes resources — including status checks, watch events, and custom CRDs — without connecting to a real cluster.

## Prerequisites

- `openshift-python-wrapper` installed (see [Installing and Creating Your First Resource](quickstart.html))
- `pytest` installed for running tests

## Quick Example

```python
import pytest
from ocp_resources.resource import get_client
from ocp_resources.namespace import Namespace

@pytest.fixture(scope="class")
def fake_client():
    return get_client(fake=True)

class TestMyFeature:
    def test_create_namespace(self, fake_client):
        ns = Namespace(client=fake_client, name="my-namespace")
        ns.deploy()

        assert ns.exists
        assert ns.kind == "Namespace"
        assert ns.status == Namespace.Status.ACTIVE

        ns.clean_up(wait=False)
        assert not ns.exists
```

Run it with `pytest` — no cluster, no kubeconfig, no network calls.

## How It Works

Calling `get_client(fake=True)` returns an in-memory client that simulates the Kubernetes API. It supports all standard resources (Pods, Deployments, Services, Namespaces, etc.) and OpenShift resources (Routes, Projects, etc.) out of the box.

All resource classes (`Pod`, `Deployment`, `Namespace`, etc.) accept this fake client through their `client` parameter, and all CRUD methods work identically to a real cluster.

## Step-by-Step: CRUD Operations

### 1. Set up a shared fixture

Create a `conftest.py` in your test directory:

```python
import pytest
from ocp_resources.resource import get_client

@pytest.fixture(scope="class")
def fake_client():
    """Provides a fake Kubernetes client for all tests."""
    return get_client(fake=True)
```

> **Tip:** Use `scope="class"` so resources created in one test method persist for subsequent methods in the same class.

### 2. Create a resource

```python
from ocp_resources.pod import Pod

def test_create_pod(fake_client):
    pod = Pod(
        client=fake_client,
        name="my-pod",
        namespace="default",
        containers=[{"name": "nginx", "image": "nginx:latest"}],
    )
    result = pod.deploy()

    assert result.name == "my-pod"
    assert pod.exists
```

The fake client automatically generates metadata (`uid`, `resourceVersion`, `creationTimestamp`) and a realistic `status` section for the resource.

### 3. Read and query resources

```python
from ocp_resources.namespace import Namespace

def test_list_resources(fake_client):
    # Create some namespaces first
    Namespace(client=fake_client, name="ns-a").deploy()
    Namespace(client=fake_client, name="ns-b").deploy()

    # Iterate over all Namespace resources
    for ns in Namespace.get(client=fake_client):
        assert ns.name
```

Access individual resource data via the `instance` property:

```python
def test_get_instance(fake_client):
    ns = Namespace(client=fake_client, name="test-ns")
    ns.deploy()

    assert ns.instance
    assert ns.kind == "Namespace"
```

### 4. Update a resource

Use `update()` to patch a resource or `update_replace()` to replace it entirely:

```python
def test_update_labels(fake_client):
    ns = Namespace(client=fake_client, name="labeled-ns")
    ns.deploy()

    # Patch: add a label
    resource_dict = ns.instance.to_dict()
    resource_dict["metadata"]["labels"]["env"] = "staging"
    ns.update(resource_dict=resource_dict)

    assert ns.labels["env"] == "staging"
```

```python
def test_replace_labels(fake_client):
    ns = Namespace(client=fake_client, name="replace-ns")
    ns.deploy()

    resource_dict = ns.instance.to_dict()
    resource_dict["metadata"]["labels"] = {"new-label": "value"}
    ns.update_replace(resource_dict=resource_dict)

    assert "new-label" in ns.labels.keys()
```

### 5. Delete a resource

```python
def test_delete_resource(fake_client):
    ns = Namespace(client=fake_client, name="temp-ns")
    ns.deploy()
    assert ns.exists

    ns.clean_up(wait=False)
    assert not ns.exists
```

### 6. Use context managers for automatic cleanup

Resources support Python's `with` statement. The resource deploys on entry and is cleaned up on exit:

```python
from ocp_resources.secret import Secret

def test_context_manager(fake_client):
    with Secret(name="my-secret", namespace="default", client=fake_client) as sec:
        assert sec.exists

    # Automatically cleaned up
    assert not sec.exists
```

## Testing Status and Conditions

The fake client generates realistic status objects so you can test code that reads resource status.

### Checking resource status

```python
def test_namespace_status(fake_client):
    ns = Namespace(client=fake_client, name="status-ns")
    ns.deploy()

    assert ns.status == Namespace.Status.ACTIVE
```

### Waiting for conditions

```python
from ocp_resources.pod import Pod

def test_wait_for_condition(fake_client):
    pod = Pod(
        client=fake_client,
        name="ready-pod",
        namespace="default",
        containers=[{"name": "app", "image": "nginx:latest"}],
    )
    pod.deploy()

    pod.wait_for_status(status=Pod.Status.RUNNING, timeout=5)
```

### Simulating "not ready" resources

Control a resource's readiness state with the `fake-client.io/ready` annotation:

```python
def test_not_ready_pod(fake_client):
    pod = Pod(
        client=fake_client,
        name="failing-pod",
        namespace="default",
        containers=[{"name": "app", "image": "nginx:latest"}],
        annotations={"fake-client.io/ready": "false"},
    )
    pod.deploy()

    # The pod will have Ready condition set to False
    pod.wait_for_condition(
        condition=pod.Condition.READY,
        status=pod.Condition.Status.FALSE,
        timeout=5,
    )

    message = pod.get_condition_message(
        condition_type=pod.Condition.READY,
        condition_status=pod.Condition.Status.FALSE,
    )
    assert message
```

Resource-specific status behavior when marked "not ready":

| Resource | Ready behavior | Not-ready behavior |
|---|---|---|
| Pod | `phase: Running`, containers ready | Containers in `waiting` state |
| Deployment | `readyReplicas` matches `spec.replicas` | `readyReplicas: 0`, `unavailableReplicas` set |
| Namespace | `phase: Active` | `phase: Terminating` |
| All others | `Ready` condition `True` | `Ready` condition `False` |

## Testing with Events

The fake client automatically generates Kubernetes events for create, update, and delete operations. You can query them through the resource's `events()` method:

```python
def test_resource_events(fake_client):
    pod = Pod(
        client=fake_client,
        name="event-pod",
        namespace="default",
        containers=[{"name": "nginx", "image": "nginx:latest"}],
    )
    pod.deploy()

    events = list(pod.events(timeout=1))
    assert events
```

## Testing with Deployments

Deployments get a full status template including replica counts, conditions, and observed generation:

```python
from ocp_resources.deployment import Deployment

def test_deployment(fake_client):
    dep = Deployment(
        client=fake_client,
        name="my-deploy",
        namespace="default",
        replicas=3,
        selector={"matchLabels": {"app": "web"}},
        template={
            "metadata": {"labels": {"app": "web"}},
            "spec": {"containers": [{"name": "web", "image": "nginx:latest"}]},
        },
    )
    dep.deploy()

    assert dep.exists
    assert dep.kind == "Deployment"

    dep.clean_up(wait=False)
    assert not dep.exists
```

## Testing with ResourceList

Test bulk resource creation with `ResourceList` and `NamespacedResourceList`:

```python
from ocp_resources.resource import ResourceList, NamespacedResourceList

def test_resource_list(fake_client):
    with ResourceList(
        client=fake_client,
        resource_class=Namespace,
        name="batch-ns",
        num_resources=3,
    ) as namespaces:
        assert len(namespaces) == 3
    # All three namespaces are automatically cleaned up
```

See [Managing Bulk Resources with ResourceList](managing-resource-lists.html) for more details.

## Advanced Usage

### Using the low-level API directly

You can bypass resource classes and work with the fake client's API directly — useful for testing custom logic:

```python
def test_low_level_api(fake_client):
    api = fake_client.resources.get(api_version="v1", kind="Pod")

    # Create
    pod = api.create(
        body={
            "apiVersion": "v1",
            "kind": "Pod",
            "metadata": {"name": "raw-pod", "namespace": "default"},
            "spec": {"containers": [{"name": "app", "image": "nginx:latest"}]},
        },
        namespace="default",
    )
    assert pod.metadata.name == "raw-pod"
    assert pod.status.phase == "Running"

    # List
    pods = api.get(namespace="default")
    assert len(pods.items) >= 1

    # Patch
    updated = api.patch(
        name="raw-pod",
        namespace="default",
        body={"metadata": {"labels": {"env": "test"}}},
    )
    assert updated.metadata.labels["env"] == "test"

    # Delete
    api.delete(name="raw-pod", namespace="default")
```

### Label and field selectors

The fake client supports filtering resources by labels and fields:

```python
def test_label_selector(fake_client):
    api = fake_client.resources.get(api_version="v1", kind="Pod")

    api.create(body={
        "apiVersion": "v1", "kind": "Pod",
        "metadata": {"name": "pod-a", "namespace": "default", "labels": {"app": "web"}},
        "spec": {"containers": [{"name": "c", "image": "nginx"}]},
    }, namespace="default")

    api.create(body={
        "apiVersion": "v1", "kind": "Pod",
        "metadata": {"name": "pod-b", "namespace": "default", "labels": {"app": "api"}},
        "spec": {"containers": [{"name": "c", "image": "nginx"}]},
    }, namespace="default")

    # Filter by label
    web_pods = api.get(namespace="default", label_selector="app=web")
    assert len(web_pods.items) == 1
    assert web_pods.items[0].metadata.name == "pod-a"
```

Field selectors support `metadata.name` and `metadata.namespace`:

```python
def test_field_selector(fake_client):
    api = fake_client.resources.get(api_version="v1", kind="Pod")
    result = api.get(namespace="default", field_selector="metadata.name=my-pod")
```

### Watch for resource changes

The `watch()` method yields existing resources as `ADDED` events:

```python
def test_watch(fake_client):
    api = fake_client.resources.get(api_version="v1", kind="Pod")

    api.create(body={
        "apiVersion": "v1", "kind": "Pod",
        "metadata": {"name": "watch-pod", "namespace": "default"},
        "spec": {"containers": [{"name": "c", "image": "nginx"}]},
    }, namespace="default")

    for event in api.watch(namespace="default"):
        assert event["type"] == "ADDED"
        assert event["object"].metadata.name
        break  # Stop after first event
```

### Registering Custom Resource Definitions

To test with CRDs not bundled in the default schema, register them on the client:

```python
def test_custom_resource(fake_client):
    # Register the CRD
    fake_client.register_resources({
        "kind": "MyApp",
        "api_version": "v1alpha1",
        "group": "apps.example.com",
        "namespaced": True,
    })

    # Use it through the low-level API
    api = fake_client.resources.get(
        api_version="apps.example.com/v1alpha1",
        kind="MyApp",
    )

    created = api.create(
        body={
            "apiVersion": "apps.example.com/v1alpha1",
            "kind": "MyApp",
            "metadata": {"name": "my-app", "namespace": "default"},
            "spec": {"replicas": 2},
        },
        namespace="default",
    )
    assert created.metadata.name == "my-app"
```

You can also register multiple CRDs at once:

```python
fake_client.register_resources([
    {"kind": "MyApp", "api_version": "v1", "group": "apps.example.com"},
    {"kind": "MyConfig", "api_version": "v1beta1", "group": "config.example.com", "namespaced": False},
])
```

### Testing incremental workflows

Use `@pytest.mark.incremental` to test a resource through its full lifecycle. Each test depends on the previous one succeeding:

```python
import pytest
from ocp_resources.pod import Pod

@pytest.mark.incremental
class TestPodLifecycle:
    @pytest.fixture(scope="class")
    def pod(self, fake_client):
        return Pod(
            client=fake_client,
            name="lifecycle-pod",
            namespace="default",
            containers=[{"name": "nginx", "image": "nginx:latest"}],
        )

    def test_01_create(self, pod):
        deployed = pod.deploy()
        assert deployed.name == "lifecycle-pod"
        assert pod.exists

    def test_02_read(self, pod):
        assert pod.instance
        assert pod.kind == "Pod"

    def test_03_update(self, pod):
        resource_dict = pod.instance.to_dict()
        resource_dict["metadata"]["labels"] = {"updated": "true"}
        pod.update(resource_dict=resource_dict)
        assert pod.labels["updated"] == "true"

    def test_04_delete(self, pod):
        pod.clean_up(wait=False)
        assert not pod.exists
```

> **Note:** The `@pytest.mark.incremental` marker requires custom `conftest.py` hooks. Add the following to your `conftest.py`:
> ```python
> def pytest_runtest_makereport(item, call):
>     if call.excinfo is not None and "incremental" in item.keywords:
>         parent = item.parent
>         parent._previousfailed = item
>
> def pytest_runtest_setup(item):
>     if "incremental" in item.keywords:
>         previousfailed = getattr(item.parent, "_previousfailed", None)
>         if previousfailed is not None:
>             pytest.xfail(f"previous test failed ({previousfailed.name})")
> ```

### Conflict detection

The fake client raises errors for duplicate creates and missing resources, just like a real cluster:

```python
from fake_kubernetes_client import NotFoundError, ConflictError

def test_conflict_on_duplicate_create(fake_client):
    api = fake_client.resources.get(api_version="v1", kind="Namespace")

    api.create(body={"metadata": {"name": "unique-ns"}})

    with pytest.raises(ConflictError):
        api.create(body={"metadata": {"name": "unique-ns"}})

def test_not_found_on_missing(fake_client):
    api = fake_client.resources.get(api_version="v1", kind="Pod")

    with pytest.raises(NotFoundError):
        api.get(name="does-not-exist", namespace="default")
```

### Accessing response data

The fake client returns response objects that support both attribute access and dictionary access:

```python
def test_response_access(fake_client):
    api = fake_client.resources.get(api_version="v1", kind="Pod")

    created = api.create(
        body={
            "apiVersion": "v1", "kind": "Pod",
            "metadata": {"name": "access-pod", "namespace": "default"},
            "spec": {"containers": [{"name": "c", "image": "nginx"}]},
        },
        namespace="default",
    )

    # Attribute access
    assert created.metadata.name == "access-pod"
    assert created.status.phase == "Running"

    # Dictionary-style access
    assert created["metadata"]["name"] == "access-pod"

    # Convert to a plain dict
    data = created.to_dict()
    assert isinstance(data, dict)
    assert data["metadata"]["name"] == "access-pod"
```

## Troubleshooting

**`ResourceNotFoundError` when creating a resource**

The fake client must recognize the resource's API group and kind. Standard Kubernetes and OpenShift resources are pre-registered. For CRDs, call `fake_client.register_resources(...)` before using them.

**Status fields are missing or empty**

Pods, Deployments, Services, and Namespaces have hardcoded status templates. Other resources get a generic status with a `Ready` condition. If the resource has a schema in the bundled OpenAPI definitions, a richer status is generated automatically.

**Tests interfere with each other**

Each call to `get_client(fake=True)` creates an independent in-memory store. Use separate fixtures or narrower fixture scopes to isolate tests that share a client.

## Related Pages

- [Connecting to Clusters](connecting-to-clusters.html)
- [Creating and Managing Resources](creating-and-managing-resources.html)
- [Querying and Watching Resources](querying-resources.html)
- [Managing Bulk Resources with ResourceList](managing-resource-lists.html)
- [Common Resource Patterns](common-patterns.html)

---

Source: generating-resource-classes.md

Let me start by exploring the repository structure and the knowledge graph.

Now let me check the generated output example and the test generation flow:

Now let me look at a generated file with a spec section, and check one with duplicate kinds:

Let me also check the `--help` output structure and look at a few more details:

Now let me check a dry-run example and verify what shell completion setup looks like:

Now I have a thorough understanding of the codebase. Let me write the documentation:

# Generating New Resource Classes with class-generator

Scaffold Python wrapper classes for any Kubernetes or OpenShift custom resource definition (CRD) so you can manage those resources using the same patterns as built-in resources. The `class-generator` CLI introspects your cluster's OpenAPI schemas and produces ready-to-use Python files complete with typed constructors, docstrings, and `to_dict()` serialization.

## Prerequisites

- **openshift-python-wrapper** installed (see [Installing and Creating Your First Resource](quickstart.html))
- `oc` or `kubectl` CLI available on your `PATH`
- An active connection to a Kubernetes or OpenShift cluster with admin privileges (for `--kind` generation and schema updates)

## Quick Example

Generate a wrapper class for the `Deployment` resource:

```bash
class-generator -k Deployment
```

This creates a Python file in `ocp_resources/` with a fully typed class you can import and use immediately:

```python
from ocp_resources.deployment import Deployment

dep = Deployment(
    name="my-app",
    namespace="default",
    replicas=3,
    selector={"matchLabels": {"app": "my-app"}},
    template={...},
)
dep.deploy()
```

## Step-by-Step: Generating a Resource Class

### 1. Generate the class file

Pass the CRD's `Kind` (case-sensitive) to `--kind` / `-k`:

```bash
class-generator -k StorageCluster
```

The output file is automatically named using snake_case — `ocp_resources/storage_cluster.py`.

### 2. Preview without writing (dry run)

See exactly what would be generated without touching the filesystem:

```bash
class-generator -k StorageCluster --dry-run
```

The rendered Python code is printed to the console with syntax highlighting.

### 3. Review the generated file

Every generated file contains:

- A class inheriting from `Resource` (cluster-scoped) or `NamespacedResource` (namespace-scoped)
- Typed `__init__` parameters extracted from the OpenAPI spec and `status` fields
- A `to_dict()` method that serializes required and optional fields
- A `# End of generated code` marker — any code you add **below** this line is preserved on regeneration

Example generated output for `ConfigMap`:

```python
from typing import Any
from ocp_resources.resource import NamespacedResource

class ConfigMap(NamespacedResource):
    """
    ConfigMap holds configuration data for pods to consume.
    """

    api_version: str = NamespacedResource.ApiVersion.V1

    def __init__(
        self,
        binary_data: dict[str, Any] | None = None,
        data: dict[str, Any] | None = None,
        immutable: bool | None = None,
        **kwargs: Any,
    ) -> None:
        ...
```

### 4. Generate multiple kinds at once

Pass a comma-separated list (no spaces):

```bash
class-generator -k Pod,Service,ConfigMap
```

Multiple kinds are generated in parallel for speed.

### 5. Overwrite an existing file

By default, existing files are not overwritten. Pass `--overwrite` to replace them:

```bash
class-generator -k Deployment --overwrite
```

To create a timestamped backup before overwriting:

```bash
class-generator -k Deployment --overwrite --backup
```

Backups are stored in `.backups/backup-YYYYMMDD-HHMMSS/` preserving the original directory structure.

### 6. Write to a custom output path

```bash
class-generator -k MyCustomResource -o my_package/custom_resource.py
```

## Handling Duplicate Kinds

Some Kubernetes kinds exist in multiple API groups. For example, `DNS` exists in both `config.openshift.io` and `operator.openshift.io`. When this happens, the generator automatically creates separate files with group-based suffixes:

| Kind | API Group | Generated File |
|------|-----------|----------------|
| `DNS` | `config.openshift.io` | `dns_config_openshift_io.py` |
| `DNS` | `operator.openshift.io` | `dns_operator_openshift_io.py` |

Both files define a class named `DNS` but with different `api_group` attributes. Import the one matching the API group you need.

## Adding User Code to Generated Files

Any code you add **below** the `# End of generated code` marker is preserved when you regenerate the file with `--overwrite`. This is how built-in resources like `ConfigMap` include custom properties:

```python
    # End of generated code

    @property
    def keys_to_hash(self):
        return ["data", "binaryData"]
```

Custom imports you add at the top of the file are also preserved during regeneration.

## Updating Schemas

The generator uses a local schema cache to resolve resource definitions. If a CRD is missing (e.g., you just installed a new operator), update the cache first.

### Full schema update

Fetch all schemas from the connected cluster:

```bash
class-generator --update-schema
```

> **Note:** If connected to an older cluster, existing schemas are preserved. Only new or missing resources are added.

### Single-resource schema update

Update just one resource without touching the rest:

```bash
class-generator --update-schema-for LlamaStackDistribution
```

Then generate the class:

```bash
class-generator -k LlamaStackDistribution --overwrite
```

> **Warning:** `--update-schema` and `--update-schema-for` are mutually exclusive. Use one or the other.

## Discovering Missing Resources

Find resources on your cluster that don't have wrapper classes yet:

```bash
class-generator --discover-missing
```

### Coverage report

Get a summary of how many resources have wrapper classes:

```bash
class-generator --coverage-report
```

Output in JSON for CI/CD pipelines:

```bash
class-generator --coverage-report --json
```

### Auto-generate all missing resources

Update schemas and generate classes for every resource that's missing:

```bash
class-generator --update-schema --generate-missing
```

> **Tip:** Use `--dry-run` with `--generate-missing` to preview what would be generated without writing any files.

## Advanced Usage

### Batch regeneration

Regenerate all existing generated resource classes to pick up schema changes:

```bash
class-generator --regenerate-all
```

With backups:

```bash
class-generator --regenerate-all --backup
```

Filter to a subset of resources using glob patterns:

```bash
class-generator --regenerate-all --filter "Pod*"
class-generator --regenerate-all --filter "*Service"
```

### Adding tests

Generate test files alongside the resource class:

```bash
class-generator -k Pod --add-tests
```

This creates test manifests and runs them automatically with `pytest`. Tests are placed under the test manifests directory and validate that the generated class can be parsed correctly.

### Shell completion

Add this to your `~/.bashrc` or `~/.zshrc` for tab completion:

```bash
# For zsh
if type class-generator > /dev/null; then eval "$(_CLASS_GENERATOR_COMPLETE=zsh_source class-generator)"; fi

# For bash
if type class-generator > /dev/null; then eval "$(_CLASS_GENERATOR_COMPLETE=bash_source class-generator)"; fi
```

### Verbose output

Enable debug logging to see exactly what the generator is doing:

```bash
class-generator -k Deployment -v
```

### When a kind is missing from the schema

If the kind you request is not in the local schema cache, the CLI prompts you:

```
deployment not found in schema mapping, Do you want to run --update-schema and retry? [Y/N]:
```

Answering `y` triggers a schema update and retries generation automatically.

> **Note:** The prompt only appears in interactive mode. When generating multiple kinds or running in batch mode, missing kinds are skipped with a warning. Run `--update-schema` separately beforehand in CI environments.

### API group and version warnings

If a generated resource uses an API group or version that hasn't been registered in the base `Resource` class, the generator logs a warning like:

```
Missing API Group in Resource
Please add `Resource.ApiGroup.MY_GROUP_IO = my-group.io` manually into
ocp_resources/resource.py under Resource class > ApiGroup class.
```

Follow the instructions to register the API group so the resource can connect to the correct API endpoint. See [Understanding the Resource Class Hierarchy](resource-class-hierarchy.html) for details on how `ApiGroup` and `ApiVersion` are used.

## CLI Options Reference

| Option | Description |
|--------|-------------|
| `-k`, `--kind` | Kind(s) to generate (comma-separated) |
| `-o`, `--output-file` | Custom output file path |
| `--overwrite` | Overwrite existing files |
| `--dry-run` | Preview output without writing files |
| `--add-tests` | Generate test files for the kind |
| `--update-schema` | Update all schema files from cluster |
| `--update-schema-for` | Update schema for a single kind |
| `--discover-missing` | Find resources without wrapper classes |
| `--coverage-report` | Show coverage statistics |
| `--json` | Output reports in JSON format |
| `--generate-missing` | Generate classes for all missing resources |
| `--regenerate-all` | Regenerate all existing generated classes |
| `--backup` | Create backup before overwriting/regenerating |
| `--filter` | Glob pattern to filter `--regenerate-all` |
| `-v`, `--verbose` | Enable debug logging |

For the full CLI reference, see [class-generator CLI Reference](class-generator-cli.html).

## Troubleshooting

**"Neither 'oc' nor 'kubectl' binary found in PATH"**
Install `oc` from the [OpenShift mirror](https://mirror.openshift.com/pub/openshift-v4/x86_64/clients/ocp/stable/) or `kubectl` from the [Kubernetes docs](https://kubernetes.io/docs/tasks/tools/). Ensure the binary is on your `PATH`.

**"Resource kind 'X' not found"**
The kind is not in the local schema cache. Run `class-generator --update-schema` while connected to a cluster that has the CRD installed, then retry.

**Generated filename looks wrong (e.g., `c_d_i_config.py`)**
Single-letter segments in snake_case names indicate the camelCase-to-snake_case converter doesn't recognize an acronym. The generator raises an error with instructions to add the acronym to the converter.

**"Failed to fetch OpenAPI v3 index"**
Ensure you are logged in to the cluster with sufficient privileges. The generator needs to access the `/openapi/v3` endpoint, which typically requires cluster-admin access.

## Related Pages

- [class-generator CLI Reference](class-generator-cli.html)
- [Schema Validation and Code Generation Architecture](schema-validation-internals.html)
- [Understanding the Resource Class Hierarchy](resource-class-hierarchy.html)
- [Resource and NamespacedResource API](resource-api.html)
- [Validating Resources Against OpenAPI Schemas](validating-resources.html)

---

Source: common-patterns.md

Let me start by reading the knowledge graph and the pages manifest, then explore the codebase.

Now let me look at more resource files for networking, RBAC, and DataVolume:

Now let me look at the secret, network policy, network attachment definition, and the duplicate API groups handling:

Now let me look at the duplicate API groups handling:

Now let me look at some files with duplicate kinds to understand the pattern:

Now let me look at how to exec in pods and also look at the user_defined_network and apply_yaml approach:

Now let me look at how the `yaml_file` parameter works and look at the event-related code:

Now let me check some gateway files to see the pattern for duplicate API groups:

Now let me look at the `user_defined_network.py` for newer networking resources:

Now I have enough information. Let me write the documentation page:

# Common Resource Patterns

Copy-paste recipes for the most common Kubernetes and OpenShift resource operations using openshift-python-wrapper.

> **Note:** All recipes assume you already have a connected client. See [Connecting to Clusters](connecting-to-clusters.html) for setup instructions.

```python
# Preamble used by all recipes below
from ocp_resources.resource import get_client
client = get_client()
```

---

## Create and Run a Pod

Create a pod with a single container and wait for it to start.

```python
from ocp_resources.pod import Pod

pod = Pod(
    client=client,
    name="my-nginx",
    namespace="default",
    containers=[{
        "name": "nginx",
        "image": "nginx:1.25",
        "ports": [{"containerPort": 80}],
    }],
    restart_policy="Never",
)
pod.deploy()
pod.wait_for_status(status=Pod.Status.RUNNING, timeout=120)
```

The `deploy()` method creates the pod on the cluster. `wait_for_status` polls until the pod reaches `Running`. Use `restart_policy="Always"` for long-running pods.

---

## Create a Pod as a Context Manager

Automatically clean up a pod when the block exits — ideal for tests.

```python
from ocp_resources.pod import Pod

with Pod(
    client=client,
    name="test-curl",
    namespace="default",
    containers=[{
        "name": "curl",
        "image": "curlimages/curl:8.5.0",
        "command": ["sleep", "3600"],
    }],
) as pod:
    pod.wait_for_status(status=Pod.Status.RUNNING, timeout=60)
    output = pod.execute(command=["curl", "-s", "http://httpbin.org/get"])
    print(output)
# Pod is automatically deleted here
```

The context manager calls `deploy()` on enter and `clean_up()` on exit. Set `teardown=False` to skip automatic deletion.

> **Tip:** See [Creating and Managing Resources](creating-and-managing-resources.html) for all lifecycle management patterns.

---

## Execute a Command Inside a Pod

Run a shell command inside an already-running pod and capture the output.

```python
from ocp_resources.pod import Pod

pod = Pod(client=client, name="my-nginx", namespace="default", ensure_exists=True)
output = pod.execute(command=["cat", "/etc/nginx/nginx.conf"], timeout=30)
print(output)
```

`execute()` streams output via the Kubernetes exec API. Use `container="sidecar"` to target a specific container in multi-container pods. Set `ignore_rc=True` to suppress errors from non-zero exit codes.

---

## Get Pod Logs

Retrieve logs from a running or completed pod.

```python
from ocp_resources.pod import Pod

pod = Pod(client=client, name="my-nginx", namespace="default", ensure_exists=True)

# Full logs
print(pod.log())

# Last 50 lines
print(pod.log(tail_lines=50))

# Logs from a specific container
print(pod.log(container="sidecar"))
```

The `log()` method passes kwargs through to the Kubernetes `read_namespaced_pod_log` API, so all standard parameters like `tail_lines`, `since_seconds`, and `container` are supported.

---

## Create a Deployment and Wait for Replicas

Deploy an application with multiple replicas and wait until they are all ready.

```python
from ocp_resources.deployment import Deployment

dep = Deployment(
    client=client,
    name="web-app",
    namespace="default",
    replicas=3,
    selector={"matchLabels": {"app": "web-app"}},
    template={
        "metadata": {"labels": {"app": "web-app"}},
        "spec": {
            "containers": [{
                "name": "app",
                "image": "nginx:1.25",
                "ports": [{"containerPort": 80}],
            }]
        },
    },
)
dep.deploy()
dep.wait_for_replicas(deployed=True, timeout=300)
```

`wait_for_replicas` polls until `availableReplicas == readyReplicas == updatedReplicas == spec.replicas`. Pass `deployed=False` to wait for all replicas to be scaled down.

---

## Scale a Deployment

Change the replica count of an existing deployment.

```python
from ocp_resources.deployment import Deployment

dep = Deployment(client=client, name="web-app", namespace="default", ensure_exists=True)

# Scale up
dep.scale_replicas(replica_count=5)
dep.wait_for_replicas(deployed=True, timeout=300)

# Scale down
dep.scale_replicas(replica_count=1)
dep.wait_for_replicas(deployed=True, timeout=300)
```

`scale_replicas` patches the deployment's `spec.replicas` field. Always follow with `wait_for_replicas` to confirm the rollout completes.

---

## Create a Service

Expose a deployment via a ClusterIP service.

```python
from ocp_resources.service import Service

svc = Service(
    client=client,
    name="web-app-svc",
    namespace="default",
    selector={"app": "web-app"},
    ports=[{
        "protocol": "TCP",
        "port": 80,
        "targetPort": 80,
    }],
    type="ClusterIP",
)
svc.deploy()
```

Change `type` to `"NodePort"` or `"LoadBalancer"` as needed. The `selector` must match labels on your target pods.

---

## Create an OpenShift Route

Expose a service externally via an OpenShift Route.

```python
from ocp_resources.route import Route

route = Route(
    client=client,
    name="web-app-route",
    namespace="default",
    service="web-app-svc",
)
route.deploy()

# Get the assigned hostname
print(route.host)
```

For TLS re-encrypt routes, pass `destination_ca_cert="<PEM certificate string>"`. Access the exposed service name with `route.exposed_service` and the TLS termination type with `route.termination`.

---

## Create a Namespace

Create a namespace (or use as a context manager for automatic cleanup).

```python
from ocp_resources.namespace import Namespace

# Simple creation
ns = Namespace(client=client, name="test-ns")
ns.deploy()

# As a context manager (deleted on exit)
with Namespace(client=client, name="ephemeral-ns") as ns:
    # ... do work in the namespace ...
    pass
```

`Namespace` extends `Resource` (cluster-scoped), so no `namespace` parameter is needed.

---

## Create a ConfigMap

Store configuration data for pods to consume.

```python
from ocp_resources.config_map import ConfigMap

cm = ConfigMap(
    client=client,
    name="app-config",
    namespace="default",
    data={
        "DATABASE_URL": "postgres://db:5432/myapp",
        "LOG_LEVEL": "info",
    },
)
cm.deploy()
```

Use `binary_data` for non-UTF-8 content. Set `immutable=True` to prevent changes after creation.

---

## Create a Secret

Store sensitive data such as credentials.

```python
from ocp_resources.secret import Secret

secret = Secret(
    client=client,
    name="db-credentials",
    namespace="default",
    string_data={
        "username": "admin",
        "password": "s3cur3-pa$$word",
    },
    type="Opaque",
)
secret.deploy()
```

Use `data_dict` instead of `string_data` if your values are already base64-encoded. Secret data is automatically hashed in log output for security.

---

## Create a Job

Run a one-off task to completion.

```python
from ocp_resources.job import Job

job = Job(
    client=client,
    name="db-migration",
    namespace="default",
    backoff_limit=3,
    restart_policy="Never",
    containers=[{
        "name": "migrate",
        "image": "my-app:latest",
        "command": ["python", "manage.py", "migrate"],
    }],
)
job.deploy()
job.wait_for_condition(
    condition=Job.Condition.COMPLETE,
    status=Job.Condition.Status.TRUE,
    timeout=300,
)
```

Set `background_propagation_policy="Background"` to delete leftover pods when the job is cleaned up. The `backoff_limit` controls how many times the job retries on failure.

---

## Create a PersistentVolumeClaim

Request persistent storage for your workloads.

```python
from ocp_resources.persistent_volume_claim import PersistentVolumeClaim

pvc = PersistentVolumeClaim(
    client=client,
    name="app-data",
    namespace="default",
    accessmodes=PersistentVolumeClaim.AccessMode.RWO,
    size="10Gi",
    storage_class="gp3-csi",
    volume_mode=PersistentVolumeClaim.VolumeMode.FILE,
)
pvc.deploy()
pvc.wait_for_status(status=PersistentVolumeClaim.Status.BOUND, timeout=120)
```

Use the `AccessMode` and `VolumeMode` constants instead of raw strings. The `storage_class` must match an available StorageClass on your cluster.

---

## Create a DataVolume (KubeVirt / CDI)

Import a VM disk image into a PVC using the Containerized Data Importer.

```python
from ocp_resources.datavolume import DataVolume

dv = DataVolume(
    client=client,
    name="fedora-disk",
    namespace="default",
    source_dict={"http": {"url": "https://download.fedoraproject.org/pub/fedora/linux/releases/40/Cloud/x86_64/images/Fedora-Cloud-Base-40-1.14.x86_64.qcow2"}},
    size="30Gi",
    storage_class="ocs-storagecluster-ceph-rbd-virtualization",
    access_modes=DataVolume.AccessMode.RWX,
    volume_mode=DataVolume.VolumeMode.BLOCK,
    api_name="storage",
)
dv.deploy()
dv.wait_for_dv_success(timeout=600)
```

Always pass `api_name="storage"` explicitly — the default will change in a future release. Use `source_dict={"blank": {}}` for empty disks, or `source_dict={"pvc": {"name": "source-pvc", "namespace": "default"}}` for cloning.

- **Clone a DataVolume:**
  ```python
  dv_clone = DataVolume(
      client=client,
      name="fedora-disk-clone",
      namespace="default",
      source_dict={"pvc": {"name": "fedora-disk", "namespace": "default"}},
      size="30Gi",
      storage_class="ocs-storagecluster-ceph-rbd-virtualization",
      access_modes=DataVolume.AccessMode.RWX,
      volume_mode=DataVolume.VolumeMode.BLOCK,
      api_name="storage",
  )
  ```

> **Tip:** See [Working with Virtual Machines (KubeVirt)](working-with-virtual-machines.html) for full VM lifecycle recipes.

---

## Create a NetworkPolicy

Restrict traffic to pods matching a label selector.

```python
from ocp_resources.network_policy import NetworkPolicy

netpol = NetworkPolicy(
    client=client,
    name="allow-http-only",
    namespace="default",
    pod_selector={"matchLabels": {"app": "web-app"}},
    policy_types=["Ingress"],
    ingress=[{
        "ports": [{"protocol": "TCP", "port": 80}],
        "from": [
            {"namespaceSelector": {"matchLabels": {"env": "production"}}},
        ],
    }],
)
netpol.deploy()
```

This allows TCP port 80 ingress only from namespaces labeled `env=production`. Omit `ingress` and set `policy_types=["Ingress"]` to deny all inbound traffic.

---

## Create a NetworkAttachmentDefinition

Attach pods to a secondary network using Multus.

```python
from ocp_resources.network_attachment_definition import LinuxBridgeNetworkAttachmentDefinition

nad = LinuxBridgeNetworkAttachmentDefinition(
    client=client,
    name="br-100",
    namespace="default",
    bridge_name="br-100",
    cni_type="cnv-bridge",
    vlan=100,
    mtu=1500,
)
nad.deploy()
```

Variant classes are available for different bridge types:
- `LinuxBridgeNetworkAttachmentDefinition` — Linux bridge (cnv-bridge)
- `OvsBridgeNetworkAttachmentDefinition` — OVS bridge
- `OVNOverlayNetworkAttachmentDefinition` — OVN overlay (layer 2/3)

---

## Create an RBAC Role and RoleBinding

Grant specific permissions to a service account within a namespace.

```python
from ocp_resources.role import Role
from ocp_resources.role_binding import RoleBinding
from ocp_resources.service_account import ServiceAccount

sa = ServiceAccount(
    client=client,
    name="app-sa",
    namespace="default",
)
sa.deploy()

role = Role(
    client=client,
    name="pod-reader",
    namespace="default",
    rules=[{
        "apiGroups": [""],
        "resources": ["pods", "pods/log"],
        "verbs": ["get", "list", "watch"],
    }],
)
role.deploy()

binding = RoleBinding(
    client=client,
    name="app-sa-pod-reader",
    namespace="default",
    subjects_kind="ServiceAccount",
    subjects_name="app-sa",
    subjects_namespace="default",
    role_ref_kind="Role",
    role_ref_name="pod-reader",
)
binding.deploy()
```

This grants the `app-sa` service account read-only access to pods in the `default` namespace.

---

## Create a ClusterRole and ClusterRoleBinding

Grant cluster-wide permissions to a service account.

```python
from ocp_resources.cluster_role import ClusterRole
from ocp_resources.cluster_role_binding import ClusterRoleBinding

cr = ClusterRole(
    client=client,
    name="node-viewer",
    rules=[{
        "apiGroups": [""],
        "resources": ["nodes"],
        "verbs": ["get", "list", "watch"],
    }],
)
cr.deploy()

crb = ClusterRoleBinding(
    client=client,
    name="app-sa-node-viewer",
    cluster_role="node-viewer",
    subjects=[{
        "kind": "ServiceAccount",
        "name": "app-sa",
        "namespace": "default",
    }],
)
crb.deploy()
```

`ClusterRole` and `ClusterRoleBinding` are cluster-scoped (`Resource` subclasses), so no `namespace` parameter is needed on the role or binding itself.

---

## List and Filter Resources

Query existing resources using label and field selectors.

```python
from ocp_resources.pod import Pod
from ocp_resources.namespace import Namespace

# List all pods in a namespace
for pod in Pod.get(client=client, namespace="default"):
    print(f"{pod.name} — {pod.status}")

# Filter by label
for pod in Pod.get(client=client, namespace="default", label_selector="app=web-app"):
    print(pod.name)

# Filter by field
for pod in Pod.get(client=client, namespace="default", field_selector="status.phase=Running"):
    print(pod.name)

# List cluster-scoped resources (no namespace)
for ns in Namespace.get(client=client, label_selector="env=staging"):
    print(ns.name)
```

The `get()` class method returns a generator of resource objects. Pass `raw=True` to get raw `ResourceInstance` objects instead.

> **Tip:** See [Querying and Watching Resources](querying-resources.html) for advanced querying patterns.

---

## Create a Resource from a YAML File

Load a resource definition from an existing YAML file.

```python
from ocp_resources.pod import Pod

pod = Pod(
    client=client,
    yaml_file="manifests/my-pod.yaml",
)
pod.deploy()
```

When using `yaml_file`, the `name` and `namespace` are read from the file automatically. You cannot combine `yaml_file` with `kind_dict`.

---

## Create a Resource from a Dictionary

Pass a raw Kubernetes resource dict directly.

```python
from ocp_resources.deployment import Deployment

dep = Deployment(
    client=client,
    kind_dict={
        "apiVersion": "apps/v1",
        "kind": "Deployment",
        "metadata": {
            "name": "from-dict-app",
            "namespace": "default",
        },
        "spec": {
            "replicas": 2,
            "selector": {"matchLabels": {"app": "from-dict"}},
            "template": {
                "metadata": {"labels": {"app": "from-dict"}},
                "spec": {
                    "containers": [{
                        "name": "app",
                        "image": "nginx:1.25",
                    }],
                },
            },
        },
    },
)
dep.deploy()
```

Using `kind_dict` bypasses all constructor logic in `to_dict()` — the dictionary is sent as-is to the API. This is useful when migrating existing manifests.

---

## Handle Resources with Duplicate API Groups

Some Kubernetes resource kinds (e.g., `DNS`, `Ingress`, `Gateway`) exist in multiple API groups. The wrapper provides separate modules for each variant.

```python
# DNS from config.openshift.io (cluster DNS config)
from ocp_resources.dns_config_openshift_io import DNS as DNSConfig

dns_config = DNSConfig(client=client, name="cluster", ensure_exists=True)
print(dns_config.instance.spec.baseDomain)

# DNS from operator.openshift.io (CoreDNS operator)
from ocp_resources.dns_operator_openshift_io import DNS as DNSOperator

dns_operator = DNSOperator(client=client, name="default", ensure_exists=True)
print(dns_operator.instance.spec.logLevel)
```

```python
# Ingress from config.openshift.io (cluster ingress config)
from ocp_resources.ingress_config_openshift_io import Ingress as IngressConfig

ingress = IngressConfig(client=client, name="cluster", ensure_exists=True)
print(ingress.instance.spec.domain)

# Ingress from networking.k8s.io (K8s Ingress rules)
from ocp_resources.ingress_networking_k8s_io import Ingress as K8sIngress

k8s_ingress = K8sIngress(
    client=client,
    name="my-app-ingress",
    rules=[{
        "host": "app.example.com",
        "http": {
            "paths": [{
                "path": "/",
                "pathType": "Prefix",
                "backend": {"service": {"name": "web-app-svc", "port": {"number": 80}}},
            }],
        },
    }],
)
```

The naming convention for duplicate-kind modules is `<kind>_<api_group_snake_case>.py`. Use Python import aliases (`as`) to distinguish them in your code.

**Common duplicate kinds and their modules:**

| Kind | Module | API Group |
|------|--------|-----------|
| `DNS` | `dns_config_openshift_io` | `config.openshift.io` |
| `DNS` | `dns_operator_openshift_io` | `operator.openshift.io` |
| `Ingress` | `ingress_config_openshift_io` | `config.openshift.io` |
| `Ingress` | `ingress_networking_k8s_io` | `networking.k8s.io` |
| `Gateway` | `gateway` | `networking.istio.io` |
| `Gateway` | `gateway_gateway_networking_k8s_io` | `gateway.networking.k8s.io` |
| `Gateway` | `gateway_networking_istio_io` | `networking.istio.io` |

> **Warning:** Importing the wrong module will target the wrong API group, causing `NotFoundError` or unexpected behavior. Always verify the `api_group` attribute on your resource class.

---

## Manage Multiple Similar Resources with ResourceList

Create N copies of a resource with auto-numbered names.

```python
from ocp_resources.namespace import Namespace
from ocp_resources.resource import ResourceList

with ResourceList(
    resource_class=Namespace,
    num_resources=3,
    client=client,
    name="perf-test-ns",
) as namespaces:
    # Creates: perf-test-ns-1, perf-test-ns-2, perf-test-ns-3
    for ns in namespaces:
        print(ns.name)
# All 3 namespaces are deleted on exit
```

> **Tip:** See [Managing Bulk Resources with ResourceList](managing-resource-lists.html) for `NamespacedResourceList` and other bulk patterns.

---

## Deploy One Resource Per Namespace with NamespacedResourceList

Create an identical namespaced resource in each of several namespaces.

```python
from ocp_resources.namespace import Namespace
from ocp_resources.config_map import ConfigMap
from ocp_resources.resource import ResourceList, NamespacedResourceList

with ResourceList(
    resource_class=Namespace,
    num_resources=3,
    client=client,
    name="team-ns",
) as namespaces:
    with NamespacedResourceList(
        resource_class=ConfigMap,
        namespaces=namespaces,
        client=client,
        name="shared-config",
        data={"REGION": "us-east-1"},
    ) as configmaps:
        # One ConfigMap "shared-config" in each of team-ns-1, team-ns-2, team-ns-3
        for cm in configmaps:
            print(f"{cm.namespace}/{cm.name}")
```

`NamespacedResourceList` requires a `ResourceList` of `Namespace` objects. All resources are cleaned up in reverse order on exit.

---

## Temporarily Edit a Resource with ResourceEditor

Apply temporary patches during a test and automatically restore original values.

```python
from ocp_resources.resource import ResourceEditor
from ocp_resources.config_map import ConfigMap

cm = ConfigMap(client=client, name="app-config", namespace="default", ensure_exists=True)

with ResourceEditor(
    patches={cm: {"data": {"LOG_LEVEL": "debug"}}},
    action="update",
):
    # LOG_LEVEL is now "debug"
    print(cm.instance.data.LOG_LEVEL)
# Original LOG_LEVEL is restored here
```

`ResourceEditor` backs up original values on enter and restores them on exit. Use `action="replace"` when you need to remove fields entirely rather than patch them.

> **Tip:** See [Editing Resources Temporarily with ResourceEditor](editing-resources-temporarily.html) for complete details.

---

## Validate a Resource Before Creating It

Catch schema errors before submitting to the API server.

```python
from ocp_resources.pod import Pod
from ocp_resources.exceptions import ValidationError

pod = Pod(
    client=client,
    name="validated-pod",
    namespace="default",
    containers=[{
        "name": "app",
        "image": "nginx:1.25",
    }],
    schema_validation_enabled=True,  # Auto-validate on create()
)

# Manual validation
try:
    pod.validate()
    print("Resource is valid")
except ValidationError as e:
    print(f"Validation failed: {e}")

# Or validate a raw dict without instantiation
try:
    Pod.validate_dict({
        "apiVersion": "v1",
        "kind": "Pod",
        "metadata": {"name": "test"},
        "spec": {"containers": [{"name": "app", "image": "nginx"}]},
    })
except ValidationError as e:
    print(f"Dict validation failed: {e}")
```

When `schema_validation_enabled=True`, `create()` and `update_replace()` automatically validate before sending to the API. Validation requires OpenAPI schemas to be available.

> **Tip:** See [Validating Resources Against OpenAPI Schemas](validating-resources.html) for schema setup and troubleshooting.

---

## Check If a Resource Exists

Verify a resource exists before operating on it.

```python
from ocp_resources.deployment import Deployment

dep = Deployment(client=client, name="web-app", namespace="default")

if dep.exists:
    print(f"Deployment exists, status: {dep.instance.status.availableReplicas} replicas available")
else:
    print("Deployment not found")
```

`exists` returns the resource instance if found or `None` if not. For fail-fast behavior, use `ensure_exists=True` in the constructor — it raises `ResourceNotFoundError` immediately if the resource is missing.

---

## Watch Resource Events

Stream events related to a specific resource.

```python
from ocp_resources.pod import Pod

pod = Pod(client=client, name="my-nginx", namespace="default", ensure_exists=True)

# Watch events for this pod (10 second window)
for event in pod.events(timeout=10):
    ev = event["object"]
    print(f"[{ev.type}] {ev.reason}: {ev.message}")
```

Use `field_selector` to narrow results further, for example: `field_selector="type==Warning"`. The `events()` method automatically filters by `involvedObject.name`.

---

## List Recent Events in a Namespace

Get existing events (not a watch stream) from the last N seconds.

```python
from ocp_resources.event import Event

# Warning events from the last 5 minutes
events = Event.list(
    client=client,
    namespace="default",
    field_selector="type==Warning",
    since_seconds=300,
)
for ev in events:
    print(f"[{ev.reason}] {ev.message}")
```

`Event.list()` returns a sorted list (most recent first), unlike `Event.get()` which is a live watch stream.

---

## Wait for a Resource Condition

Block until a resource reaches a specific condition.

```python
from ocp_resources.deployment import Deployment

dep = Deployment(client=client, name="web-app", namespace="default", ensure_exists=True)

dep.wait_for_condition(
    condition="Available",
    status="True",
    timeout=300,
)
```

Use `stop_condition` to fail fast if an unrecoverable condition is detected:

```python
dep.wait_for_condition(
    condition="Available",
    status="True",
    timeout=300,
    stop_condition="ReplicaFailure",
    stop_status="True",
)
```

> **Tip:** See [Waiting for Resource Conditions and Status](waiting-for-conditions.html) for advanced waiting patterns.

---

## Get a Resource's YAML Representation

Dump the intended resource dict as YAML (useful for debugging).

```python
from ocp_resources.pod import Pod

pod = Pod(
    client=client,
    name="debug-pod",
    namespace="default",
    containers=[{"name": "app", "image": "busybox", "command": ["sleep", "3600"]}],
)
print(pod.to_yaml())
```

`to_yaml()` calls `to_dict()` internally and returns the YAML string. This shows what would be sent to the API, not the live state.

---

## Get the Pod's Node and IP

Access commonly needed runtime properties of a pod.

```python
from ocp_resources.pod import Pod

pod = Pod(client=client, name="my-nginx", namespace="default", ensure_exists=True)

# Node where the pod is running
print(f"Node: {pod.node.name}")

# Pod IP address
print(f"IP: {pod.ip}")
```

The `node` property returns a `Node` resource object. The `ip` property reads from `status.podIP`.

## Related Pages

- [Creating and Managing Resources](creating-and-managing-resources.html)
- [Querying and Watching Resources](querying-resources.html)
- [Working with Virtual Machines (KubeVirt)](working-with-virtual-machines.html)
- [Executing Commands in Pods and Retrieving Logs](pod-execution-and-logs.html)
- [Working with Kubernetes Events](working-with-events.html)

---

Source: mcp-server-integration.md

# Integrating with AI Assistants via MCP Server

The openshift-python-wrapper ships with a built-in [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that lets AI assistants like Cursor, Claude Desktop, and other MCP-compatible tools directly manage your OpenShift/Kubernetes cluster resources.

## Configure the MCP Server for Cursor

Connect your Cursor IDE to your cluster so the AI assistant can list, create, update, and delete resources on your behalf.

```json
// ~/.cursor/mcp.json
{
  "mcpServers": {
    "openshift-python-wrapper": {
      "command": "openshift-mcp-server"
    }
  }
}
```

After saving, restart Cursor and reference the server with `@openshift-python-wrapper` in your prompts. The server uses your active kubeconfig context automatically.

> **Note:** The `openshift-mcp-server` command is installed as a script entry point when you install the package. See [Installing and Creating Your First Resource](quickstart.html) for installation instructions.

## Configure the MCP Server for Claude Desktop

Set up Claude Desktop to interact with your cluster through natural language.

```json
// macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
// Windows: %APPDATA%\Claude\claude_desktop_config.json
// Linux: ~/.config/Claude/claude_desktop_config.json
{
  "mcpServers": {
    "openshift-python-wrapper": {
      "command": "openshift-mcp-server"
    }
  }
}
```

Claude Desktop will connect to the server on startup. You can then ask it to list pods, create resources, check logs, and more using natural language.

## Configure a Custom Kubeconfig Path

Point the MCP server at a specific kubeconfig when your default location doesn't apply.

```json
{
  "mcpServers": {
    "openshift-python-wrapper": {
      "command": "openshift-mcp-server",
      "env": {
        "KUBECONFIG": "/home/user/.kube/my-cluster-config"
      }
    }
  }
}
```

The `KUBECONFIG` environment variable is passed to the server process at startup. This is useful when managing multiple clusters or when your kubeconfig is in a non-standard location.

> **Tip:** See [Connecting to Clusters](connecting-to-clusters.html) for all authentication methods supported by openshift-python-wrapper, including tokens, basic auth, and in-cluster config.

## Run the MCP Server from Source (Development)

Run the server directly from a cloned repository for development or testing.

```bash
git clone https://github.com/RedHatQE/openshift-python-wrapper.git
cd openshift-python-wrapper
uv run mcp_server/server.py
```

For MCP client configuration when running from source:

```json
{
  "mcpServers": {
    "openshift-python-wrapper": {
      "command": "uv",
      "args": ["run", "mcp_server/server.py"],
      "cwd": "/path/to/openshift-python-wrapper"
    }
  }
}
```

This bypasses the installed entry point and runs the server module directly, which is useful when testing local changes.

## List Cluster Resources via AI Chat

Ask your AI assistant to list resources — the MCP server exposes a `list_resources` tool that supports type filtering, namespaces, label selectors, and limits.

```
Prompt: "List all pods in the openshift-monitoring namespace with label app=prometheus"
```

The AI will call the `list_resources` tool, which maps to:

```python
list_resources(
    resource_type="pod",
    namespace="openshift-monitoring",
    label_selector="app=prometheus",
    limit=20
)
```

Returned data includes name, namespace, UID, creation timestamp, labels, phase, and conditions for each matched resource.

> **Tip:** Use `get_resource_types` to discover all available resource types. The server dynamically scans every module in `ocp_resources/` at startup, so any resource supported by the library — including OpenShift-specific types like `route`, `virtualmachine`, or `clusterserviceversion` — is available.

## Get Detailed Resource Information

Retrieve a single resource with full metadata, status, and optionally the raw YAML or JSON.

```
Prompt: "Show me the deployment 'api-server' in namespace 'production' as YAML"
```

The AI calls:

```python
get_resource(
    resource_type="deployment",
    name="api-server",
    namespace="production",
    output_format="yaml"    # also accepts "json" or "info"
)
```

The `info` format (default) returns structured metadata plus resource-specific details: for pods you get container statuses and node placement; for deployments you get replica counts; for services you get ports and cluster IP.

## Create Resources Through Natural Language

Ask the AI to create resources — it can use either YAML content or structured spec parameters.

```
Prompt: "Create a ConfigMap called 'app-settings' in the 'staging' namespace with keys
database_host=db.staging.svc and log_level=debug"
```

The AI calls:

```python
create_resource(
    resource_type="configmap",
    name="app-settings",
    namespace="staging",
    spec={"data": {"database_host": "db.staging.svc", "log_level": "debug"}},
    labels={"managed-by": "mcp-server"}
)
```

You can also provide full YAML:

```python
create_resource(
    resource_type="pod",
    name="nginx",
    yaml_content="""
apiVersion: v1
kind: Pod
metadata:
  name: nginx
  namespace: default
spec:
  containers:
  - name: nginx
    image: nginx:1.27
    ports:
    - containerPort: 80
"""
)
```

> **Warning:** The MCP server creates real resources on your cluster. Make sure your AI assistant is targeting the correct cluster and namespace before confirming creation operations.

## Apply Multi-Document YAML Manifests

Deploy a complete application stack from a single YAML block containing multiple resources.

```
Prompt: "Apply this manifest to create a namespace, deployment, and service for my app"
```

The AI calls:

```python
apply_yaml(yaml_content="""
---
apiVersion: v1
kind: Namespace
metadata:
  name: my-app
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend
  namespace: my-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: backend
  template:
    metadata:
      labels:
        app: backend
    spec:
      containers:
      - name: api
        image: myapp/backend:v1.0
        ports:
        - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: backend
  namespace: my-app
spec:
  selector:
    app: backend
  ports:
  - port: 80
    targetPort: 8080
""")
```

The response includes a summary with `total_resources`, `successful`, and `failed` counts, plus per-resource results. Each YAML document is parsed and deployed independently.

## Update Resources with Patches

Modify existing resources using merge or strategic-merge patches.

```
Prompt: "Scale the 'web-frontend' deployment in 'production' to 5 replicas"
```

The AI calls:

```python
update_resource(
    resource_type="deployment",
    name="web-frontend",
    namespace="production",
    patch={"spec": {"replicas": 5}},
    patch_type="merge"       # or "strategic"
)
```

The `patch_type` parameter controls the content type: `"merge"` uses `application/merge-patch+json`, while `"strategic"` uses `application/strategic-merge-patch+json`.

## Delete Resources

Remove resources with optional wait-for-deletion behavior.

```
Prompt: "Delete the pod 'debug-pod' in namespace 'default' and wait for it to be gone"
```

The AI calls:

```python
delete_resource(
    resource_type="pod",
    name="debug-pod",
    namespace="default",
    wait=True,
    timeout=60
)
```

If the resource doesn't exist, the server returns a success response with a warning rather than an error, making the operation idempotent.

## Retrieve Pod Logs

Fetch logs from running or crashed pods with filtering options.

```
Prompt: "Show me the last 200 lines of logs from pod 'api-server-7f8b9c' container 'app'
in namespace 'production'"
```

The AI calls:

```python
get_pod_logs(
    name="api-server-7f8b9c",
    namespace="production",
    container="app",
    tail_lines=200
)
```

- Use `since_seconds=3600` to get logs from the last hour
- Use `previous=True` to get logs from a crashed container's previous instance
- Omit `container` for single-container pods

## Execute Commands in Pods

Run diagnostic commands inside a running pod container.

```
Prompt: "Check the nginx config in pod 'web-server' namespace 'production'"
```

The AI calls:

```python
exec_in_pod(
    name="web-server",
    namespace="production",
    command=["nginx", "-t"],
    container="nginx"
)
```

The response includes `stdout`, `stderr`, and `returncode`. If the command fails, the exit code and error output are captured rather than raising an exception.

> **Warning:** Pod exec gives shell-level access to your containers. Ensure your RBAC policies restrict which service accounts and users can perform `exec` operations.

## Get Resource Events

Retrieve Kubernetes events related to a specific resource for troubleshooting.

```
Prompt: "Show me the events for pod 'crashloop-pod' in 'default' namespace"
```

The AI calls:

```python
get_resource_events(
    resource_type="pod",
    name="crashloop-pod",
    namespace="default",
    limit=10
)
```

Events are filtered using field selectors on `involvedObject.name`, `involvedObject.namespace`, and `involvedObject.kind`. Each event includes type (Normal/Warning), reason, message, count, timestamps, and source component.

## Troubleshoot a Failing Pod (End-to-End Workflow)

Combine multiple MCP tools in sequence to diagnose pod issues — the AI assistant will chain these calls automatically.

```
Prompt: "Pod 'payment-svc-6d4f8' in namespace 'checkout' keeps crashing. Help me figure out why."
```

The AI will typically execute this sequence:

1. **Check status:** `get_resource(resource_type="pod", name="payment-svc-6d4f8", namespace="checkout")` — gets phase, container statuses, restart count
2. **Review events:** `get_resource_events(resource_type="pod", name="payment-svc-6d4f8", namespace="checkout")` — shows scheduling, pulling, crash events
3. **Read current logs:** `get_pod_logs(name="payment-svc-6d4f8", namespace="checkout", tail_lines=100)` — application output before crash
4. **Read previous crash logs:** `get_pod_logs(name="payment-svc-6d4f8", namespace="checkout", previous=True)` — logs from the last terminated container
5. **Inspect config:** `exec_in_pod(name="payment-svc-6d4f8", namespace="checkout", command=["cat", "/etc/app/config.yaml"])` — verify mounted configuration

The AI then synthesizes all the data into a diagnosis with suggested fixes.

## Write a Programmatic MCP Client

Connect to the MCP server programmatically using `FastMCPClient` for automation scripts.

```python
import asyncio
from fastmcp import FastMCPClient


async def main():
    async with FastMCPClient() as client:
        await client.connect_stdio(cmd=["openshift-mcp-server"])

        # Discover available resource types
        types = await client.call_tool(
            name="get_resource_types",
            arguments={"random_string": "x"},
        )
        print(f"Available types: {types['total_count']}")

        # List pods in a namespace
        pods = await client.call_tool(
            name="list_resources",
            arguments={"resource_type": "pod", "namespace": "default", "limit": 5},
        )
        for pod in pods["resources"]:
            print(f"  {pod['name']} — {pod.get('phase', 'Unknown')}")


if __name__ == "__main__":
    asyncio.run(main())
```

This uses the same stdio transport that AI assistants use. The `FastMCPClient` is provided by the `fastmcp` library, which is already a dependency of openshift-python-wrapper.

## Debug MCP Server Issues

Enable debug logging and inspect the server log file when tools aren't behaving as expected.

```bash
# Check the MCP server log file
tail -f /tmp/mcp_server_debug.log
```

```bash
# Run with debug-level logging from the simple-logger
SIMPLE_LOGGER_LEVEL=DEBUG openshift-mcp-server
```

The server writes debug logs to `/tmp/mcp_server_debug.log` by default, including every resource scan at startup, client creation events, and detailed error tracebacks.

- Verify cluster connectivity independently: `kubectl cluster-info`
- Check your permissions: `kubectl auth can-i --list`
- Confirm the entry point is installed: `which openshift-mcp-server`

> **Tip:** See [Environment Variables and Configuration](environment-variables.html) for other environment variables that affect library behavior.

## Available MCP Tools Reference

The MCP server exposes ten tools. All resource type names are case-insensitive (e.g., `"Pod"`, `"pod"`, and `"POD"` all work).

| Tool | Purpose | Required Parameters |
|------|---------|-------------------|
| `list_resources` | List resources by type with optional filters | `resource_type` |
| `get_resource` | Get a single resource by name | `resource_type`, `name` |
| `create_resource` | Create a resource from spec or YAML | `resource_type`, `name`, plus `spec` or `yaml_content` |
| `update_resource` | Patch an existing resource | `resource_type`, `name`, `patch` |
| `delete_resource` | Delete a resource | `resource_type`, `name` |
| `get_pod_logs` | Retrieve pod container logs | `name`, `namespace` |
| `exec_in_pod` | Execute a command in a pod | `name`, `namespace`, `command` |
| `get_resource_events` | Get events for a resource | `resource_type`, `name` |
| `apply_yaml` | Apply one or more YAML documents | `yaml_content` |
| `get_resource_types` | List all discovered resource types | `random_string` (any value) |

> **Note:** Namespaced resources (pods, deployments, configmaps, etc.) require a `namespace` parameter. Cluster-scoped resources (nodes, namespaces, cluster roles) do not. The server validates this automatically and returns a clear error if a namespace is missing.

## Related Pages

- [Connecting to Clusters](connecting-to-clusters.html)
- [Installing and Creating Your First Resource](quickstart.html)
- [Environment Variables and Configuration](environment-variables.html)
- [Executing Commands in Pods and Retrieving Logs](pod-execution-and-logs.html)
- [Querying and Watching Resources](querying-resources.html)

---

Source: resource-api.md

Let me start by reading the knowledge graph and the pages manifest, then explore the source code.

Now let me examine the constants and exceptions:

Now let me check the `apply_yaml` function and the `get_client` function signature, plus see what `ResourceConstants` inherits:

Let me look for key public methods in resource.py that I might have missed (the file was truncated before):

Let me also check for import paths:

Now let me look at the `NamespacedResource._base_body` more carefully and check for anything more:

Now let me check what the `Event` class looks like for the events method reference:

Let me also check for `generate_yaml_from_template`:

That's a test utility. Let me check for the `skip_existing_resource_creation_teardown` function:

Now I have all the information needed. Let me write the comprehensive API reference page.

# Resource and NamespacedResource API

Complete API reference for the `Resource` and `NamespacedResource` base classes, the `get_client` factory function, and supporting classes (`ResourceEditor`, `ResourceList`, `NamespacedResourceList`, `KubeAPIVersion`).

**Import path:**

```python
from ocp_resources.resource import (
    Resource,
    NamespacedResource,
    ResourceEditor,
    ResourceList,
    NamespacedResourceList,
    get_client,
)
```

---

## `get_client`

Factory function to obtain a Kubernetes `DynamicClient`.

```python
from ocp_resources.resource import get_client

client = get_client(config_file="~/.kube/config")
```

| Parameter | Type | Default | Description |
|---|---|---|---|
| `config_file` | `str \| None` | `None` | Path to a kubeconfig file. Falls back to `KUBECONFIG` env var or `~/.kube/config`. |
| `config_dict` | `dict[str, Any] \| None` | `None` | Dict with kubeconfig configuration. Mutually exclusive with `config_file`. |
| `context` | `str \| None` | `None` | Name of the kubeconfig context to use. |
| `client_configuration` | `kubernetes.client.Configuration \| None` | `None` | Pre-built Kubernetes client configuration object. |
| `persist_config` | `bool` | `True` | Whether to persist config file changes. |
| `temp_file_path` | `str \| None` | `None` | Path to a temporary kubeconfig file. |
| `try_refresh_token` | `bool` | `True` | Try to refresh the authentication token. |
| `username` | `str \| None` | `None` | Username for basic auth. Requires `password` and `host`. |
| `password` | `str \| None` | `None` | Password for basic auth. Requires `username` and `host`. |
| `host` | `str \| None` | `None` | Cluster host URL. |
| `verify_ssl` | `bool \| None` | `None` | Whether to verify SSL certificates. |
| `token` | `str \| None` | `None` | Bearer token for authentication. Requires `host`. |
| `fake` | `bool` | `False` | Return a `FakeDynamicClient` for testing without a cluster. |
| `generate_kubeconfig` | `bool` | `False` | Save the resolved kubeconfig to a temp file and attach the path to the client. |

**Returns:** `DynamicClient | FakeDynamicClient`

```python
# Default kubeconfig
client = get_client()

# With explicit config file and context
client = get_client(config_file="/path/to/kubeconfig", context="my-cluster")

# Token-based auth
client = get_client(host="https://api.cluster.example.com:6443", token="sha256~abc123")

# Basic auth
client = get_client(host="https://api.cluster.example.com:6443", username="admin", password="secret")

# Fake client for unit testing
client = get_client(fake=True)
```

> **Note:** If neither `config_file` nor `config_dict` is provided, the client falls back to the `KUBECONFIG` environment variable, then `~/.kube/config`, and finally in-cluster configuration.

See [Connecting to Clusters](connecting-to-clusters.html) for connection patterns. See [Testing Without a Cluster Using the Fake Client](testing-without-cluster.html) for `fake=True` usage.

---

## `Resource`

```python
from ocp_resources.resource import Resource
```

Base class for **cluster-scoped** Kubernetes/OpenShift resources (e.g., `Namespace`, `Node`, `ClusterRole`). Inherits from `ResourceConstants`. All concrete resource classes inherit from either `Resource` or `NamespacedResource`.

See [Understanding the Resource Class Hierarchy](resource-class-hierarchy.html) for the inheritance model.

### Class Attributes

| Attribute | Type | Default | Description |
|---|---|---|---|
| `api_group` | `str` | `""` | API group for the resource (e.g., `"apps"`, `"batch"`). |
| `api_version` | `str` | `""` | Full API version string (e.g., `"v1"`, `"apps/v1"`). Resolved automatically if `api_group` is set. |
| `singular_name` | `str` | `""` | Singular resource name for API calls. Used to disambiguate when multiple resources match the same kind. |
| `timeout_seconds` | `int` | `60` | Default timeout for API list/watch operations. |

### Constructor

```python
Resource(
    client=client,
    name="my-resource",
)
```

| Parameter | Type | Default | Description |
|---|---|---|---|
| `client` | `DynamicClient \| None` | `None` | Kubernetes dynamic client. Will be mandatory in the next major release. |
| `name` | `str \| None` | `None` | Resource name. Required unless `yaml_file` or `kind_dict` is provided. |
| `teardown` | `bool` | `True` | Whether this resource should be deleted when used as a context manager. |
| `yaml_file` | `str \| None` | `None` | Path to a YAML file defining the resource. Mutually exclusive with `kind_dict`. |
| `delete_timeout` | `int` | `240` | Timeout in seconds for delete operations. |
| `dry_run` | `bool` | `False` | If `True`, create operations use dry-run mode. |
| `node_selector` | `dict[str, Any] \| None` | `None` | Node selector for scheduling. |
| `node_selector_labels` | `dict[str, str] \| None` | `None` | Node selector labels for scheduling. |
| `config_file` | `str \| None` | `None` | Path to kubeconfig. Deprecated; pass `client` instead. |
| `config_dict` | `dict[str, Any] \| None` | `None` | Kubeconfig dict. |
| `context` | `str \| None` | `None` | Kubeconfig context name. Deprecated; pass `client` instead. |
| `label` | `dict[str, str] \| None` | `None` | Labels to set on the resource. |
| `annotations` | `dict[str, str] \| None` | `None` | Annotations to set on the resource. |
| `api_group` | `str` | `""` | Override the class-level `api_group`. |
| `hash_log_data` | `bool` | `True` | Hash sensitive fields (defined by `keys_to_hash`) in log output. |
| `ensure_exists` | `bool` | `False` | Check that the resource already exists on the cluster at init time. Raises `ResourceNotFoundError` if not. |
| `kind_dict` | `dict[Any, Any] \| None` | `None` | Complete resource dict. Mutually exclusive with `yaml_file`. Bypasses `to_dict()` logic. |
| `wait_for_resource` | `bool` | `False` | When used as a context manager, wait for the resource to exist after creation. |
| `schema_validation_enabled` | `bool` | `False` | Enable automatic OpenAPI schema validation on `create()` and `update_replace()`. |

**Raises:**

| Exception | Condition |
|---|---|
| `ValueError` | Both `yaml_file` and `kind_dict` are provided. |
| `NotImplementedError` | Neither `api_group` nor `api_version` is defined on the class. |
| `MissingRequiredArgumentError` | None of `name`, `yaml_file`, or `kind_dict` is provided. |
| `ResourceNotFoundError` | `ensure_exists=True` and the resource does not exist. |

```python
from ocp_resources.namespace import Namespace

# Basic creation
ns = Namespace(client=client, name="my-namespace")

# From a YAML file
ns = Namespace(client=client, yaml_file="namespace.yaml")

# From a dict
ns = Namespace(client=client, kind_dict={"apiVersion": "v1", "kind": "Namespace", "metadata": {"name": "test"}})

# With labels and annotations
ns = Namespace(client=client, name="my-ns", label={"env": "test"}, annotations={"owner": "team-a"})

# Verify it already exists
ns = Namespace(client=client, name="default", ensure_exists=True)
```

---

### Context Manager Protocol

`Resource` supports the Python context manager protocol for automatic creation and cleanup.

```python
from ocp_resources.namespace import Namespace

with Namespace(client=client, name="temp-ns") as ns:
    # Resource is created on __enter__
    print(ns.name)
# Resource is deleted on __exit__ (if teardown=True)
```

| Method | Description |
|---|---|
| `__enter__()` | Calls `deploy(wait=self.wait_for_resource)`. Registers a `SIGINT` handler on the main thread to ensure cleanup on Ctrl+C. Returns `self`. |
| `__exit__(...)` | Calls `clean_up()` if `teardown=True`. Raises `ResourceTeardownError` if cleanup fails. |

> **Tip:** Set `teardown=False` to prevent automatic deletion on context exit.

---

### CRUD Methods

#### `create`

```python
def create(
    self,
    wait: bool = False,
    exceptions_dict: dict[type[Exception], list[str]] = ...,
) -> ResourceInstance | None
```

Create the resource on the cluster.

| Parameter | Type | Default | Description |
|---|---|---|---|
| `wait` | `bool` | `False` | Wait for the resource to exist after creation. |
| `exceptions_dict` | `dict[type[Exception], list[str]]` | `DEFAULT_CLUSTER_RETRY_EXCEPTIONS \| PROTOCOL_ERROR_EXCEPTION_DICT` | Exceptions to retry on. |

**Returns:** `ResourceInstance | None`

**Raises:** `ValidationError` if `schema_validation_enabled=True` and the resource dict is invalid.

```python
ns = Namespace(client=client, name="my-ns")
ns.create(wait=True)
```

#### `delete`

```python
def delete(
    self,
    wait: bool = False,
    timeout: int = 240,
    body: dict[str, Any] | None = None,
) -> bool
```

Delete the resource from the cluster.

| Parameter | Type | Default | Description |
|---|---|---|---|
| `wait` | `bool` | `False` | Wait for the resource to be fully deleted. |
| `timeout` | `int` | `240` | Timeout in seconds when `wait=True`. |
| `body` | `dict[str, Any] \| None` | `None` | Optional delete options body. |

**Returns:** `bool` — `True` if deleted or not found; `False` only if wait timed out.

```python
ns.delete(wait=True, timeout=120)
```

#### `update`

```python
def update(self, resource_dict: dict[str, Any]) -> None
```

Patch the resource with a merge-patch (`application/merge-patch+json`). Only updates the fields present in `resource_dict`.

| Parameter | Type | Description |
|---|---|---|
| `resource_dict` | `dict[str, Any]` | Partial resource dictionary with fields to update. |

> **Note:** Schema validation is **not** applied on `update()` because patches are partial and would fail full-schema validation.

```python
ns.update(resource_dict={"metadata": {"labels": {"env": "staging"}}})
```

#### `update_replace`

```python
def update_replace(self, resource_dict: dict[str, Any]) -> None
```

Replace the full resource. Use this to **remove** existing fields (unlike `update()`, which only adds/modifies).

| Parameter | Type | Description |
|---|---|---|
| `resource_dict` | `dict[str, Any]` | Complete resource dictionary to replace with. |

**Raises:** `ValidationError` if `schema_validation_enabled=True` and the dict is invalid.

```python
full_dict = ns.instance.to_dict()
full_dict["metadata"]["labels"] = {"new-label": "only"}
ns.update_replace(resource_dict=full_dict)
```

---

### Deploy / Clean Up

#### `deploy`

```python
def deploy(self, wait: bool = False) -> Self
```

Create the resource (respects `REUSE_IF_RESOURCE_EXISTS` environment variable).

| Parameter | Type | Default | Description |
|---|---|---|---|
| `wait` | `bool` | `False` | Wait for the resource after creation. |

**Returns:** `Self`

See [Environment Variables and Configuration](environment-variables.html) for `REUSE_IF_RESOURCE_EXISTS` details.

#### `clean_up`

```python
def clean_up(self, wait: bool = True, timeout: int | None = None) -> bool
```

Delete the resource (respects `SKIP_RESOURCE_TEARDOWN` environment variable).

| Parameter | Type | Default | Description |
|---|---|---|---|
| `wait` | `bool` | `True` | Wait for deletion to complete. |
| `timeout` | `int \| None` | `None` | Timeout in seconds. Defaults to `delete_timeout`. |

**Returns:** `bool` — `True` if deleted successfully.

See [Environment Variables and Configuration](environment-variables.html) for `SKIP_RESOURCE_TEARDOWN` details.

---

### Query Methods and Properties

#### `exists`

```python
@property
def exists(self) -> ResourceInstance | None
```

Check if the resource exists on the cluster.

**Returns:** `ResourceInstance` if found, `None` if not.

```python
if ns.exists:
    print("Namespace exists")
```

#### `instance`

```python
@property
def instance(self) -> ResourceInstance
```

Get the live resource instance from the cluster. Retries on transient cluster errors.

**Returns:** `ResourceInstance`

**Raises:** `NotFoundError` if the resource does not exist.

```python
resource_version = ns.instance.metadata.resourceVersion
```

#### `status`

```python
@property
def status(self) -> str
```

Get `status.phase` from the resource instance.

**Returns:** `str` — The status phase string (e.g., `"Running"`, `"Active"`, `"Pending"`).

```python
print(ns.status)  # "Active"
```

#### `labels`

```python
@property
def labels(self) -> ResourceField
```

Get resource labels from `metadata.labels`.

**Returns:** `ResourceField`

```python
for key, value in ns.labels.items():
    print(f"{key}={value}")
```

#### `kind`

```python
@ClassProperty
def kind(cls) -> str | None
```

Get the resource kind name derived from the class hierarchy. This is a **class-level property** — accessible on both the class and instances.

**Returns:** `str | None`

```python
from ocp_resources.namespace import Namespace
print(Namespace.kind)  # "Namespace"
```

#### `api`

```python
@property
def api(self) -> ResourceInstance
```

Get the resource API object (shortcut for `full_api()` with no extra kwargs).

**Returns:** `ResourceInstance`

#### `full_api`

```python
def full_api(self, **kwargs: Any) -> ResourceInstance
```

Get the resource API object with optional filtering kwargs.

| Keyword Argument | Description |
|---|---|
| `pretty` | Pretty-print output. |
| `_continue` | Continuation token for list pagination. |
| `field_selector` | Filter by field. |
| `label_selector` | Filter by label. |
| `limit` | Maximum number of results. |
| `resource_version` | Filter by resource version. |
| `timeout_seconds` | Request timeout. |
| `watch` | Enable watch mode. |

**Returns:** `ResourceInstance`

---

### Class Method: `get`

```python
@classmethod
def get(
    cls,
    client: DynamicClient | None = None,
    dyn_client: DynamicClient | None = None,
    config_file: str = "",
    singular_name: str = "",
    exceptions_dict: dict[type[Exception], list[str]] = ...,
    raw: bool = False,
    context: str | None = None,
    *args: Any,
    **kwargs: Any,
) -> Generator[Any, None, None]
```

List resources of this kind from the cluster.

| Parameter | Type | Default | Description |
|---|---|---|---|
| `client` | `DynamicClient \| None` | `None` | Kubernetes client. |
| `dyn_client` | `DynamicClient \| None` | `None` | Deprecated alias for `client`. |
| `config_file` | `str` | `""` | Path to kubeconfig. Deprecated; pass `client`. |
| `singular_name` | `str` | `""` | Singular resource name for disambiguation. |
| `exceptions_dict` | `dict` | `DEFAULT_CLUSTER_RETRY_EXCEPTIONS` | Exceptions to retry. |
| `raw` | `bool` | `False` | If `True`, yield raw `ResourceInstance` objects instead of wrapper instances. |
| `context` | `str \| None` | `None` | Kubeconfig context. Deprecated; pass `client`. |
| `**kwargs` | | | Passed through to the API (e.g., `label_selector`, `field_selector`). |

**Returns:** `Generator` of resource instances.

> **Note:** For `Resource` (cluster-scoped), yielded objects are constructed with `name` only. For `NamespacedResource`, yielded objects include both `name` and `namespace`.

```python
from ocp_resources.namespace import Namespace

for ns in Namespace.get(client=client):
    print(ns.name)

# With label selector
for ns in Namespace.get(client=client, label_selector="env=production"):
    print(ns.name)

# Raw mode
for raw_ns in Namespace.get(client=client, raw=True):
    print(raw_ns.metadata.name, raw_ns.status.phase)
```

See [Querying and Watching Resources](querying-resources.html) for advanced list/filter patterns.

---

### Static Method: `get_all_cluster_resources`

```python
@staticmethod
def get_all_cluster_resources(
    client: DynamicClient | None = None,
    config_file: str = "",
    context: str | None = None,
    config_dict: dict[str, Any] | None = None,
    *args: Any,
    **kwargs: Any,
) -> Generator[ResourceField, None, None]
```

Yield all resources across **all** API groups in the cluster.

| Parameter | Type | Default | Description |
|---|---|---|---|
| `client` | `DynamicClient \| None` | `None` | Kubernetes client. |
| `**kwargs` | | | Passed to the API (e.g., `label_selector`). |

**Yields:** `ResourceField`

```python
for resource in Resource.get_all_cluster_resources(client=client, label_selector="app=myapp"):
    print(f"{resource.kind}: {resource.metadata.name}")
```

---

### Wait Methods

#### `wait`

```python
def wait(self, timeout: int = 240, sleep: int = 1) -> None
```

Wait until the resource exists on the cluster.

| Parameter | Type | Default | Description |
|---|---|---|---|
| `timeout` | `int` | `240` | Maximum wait time in seconds. |
| `sleep` | `int` | `1` | Sleep interval between retries. |

**Raises:** `TimeoutExpiredError` if the resource does not exist within the timeout.

#### `wait_deleted`

```python
def wait_deleted(self, timeout: int = 240) -> bool
```

Wait until the resource no longer exists.

| Parameter | Type | Default | Description |
|---|---|---|---|
| `timeout` | `int` | `240` | Maximum wait time in seconds. |

**Returns:** `bool` — `True` if deleted, `False` if timed out.

#### `wait_for_status`

```python
def wait_for_status(
    self,
    status: str,
    timeout: int = 240,
    stop_status: str | None = None,
    sleep: int = 1,
    exceptions_dict: dict[type[Exception], list[str]] = ...,
) -> None
```

Wait for `status.phase` to reach the expected value.

| Parameter | Type | Default | Description |
|---|---|---|---|
| `status` | `str` | — | Expected status phase string. |
| `timeout` | `int` | `240` | Maximum wait time in seconds. |
| `stop_status` | `str \| None` | `None` | Stop and fail immediately if this status is reached. Defaults to `Status.FAILED`. |
| `sleep` | `int` | `1` | Sleep interval between retries. |
| `exceptions_dict` | `dict` | `PROTOCOL_ERROR_EXCEPTION_DICT \| DEFAULT_CLUSTER_RETRY_EXCEPTIONS` | Exceptions to retry. |

**Raises:** `TimeoutExpiredError` if the desired status is not reached.

```python
from ocp_resources.pod import Pod

pod.wait_for_status(status=Pod.Status.RUNNING, timeout=120)
```

#### `wait_for_condition`

```python
def wait_for_condition(
    self,
    condition: str,
    status: str,
    timeout: int = 300,
    sleep_time: int = 1,
    reason: str | None = None,
    message: str = "",
    stop_condition: str | None = None,
    stop_status: str = "True",
) -> None
```

Wait for a specific condition to reach the desired state.

| Parameter | Type | Default | Description |
|---|---|---|---|
| `condition` | `str` | — | Condition type name (e.g., `"Ready"`, `"Available"`). |
| `status` | `str` | — | Expected condition status (e.g., `"True"`, `"False"`). |
| `timeout` | `int` | `300` | Maximum wait time in seconds. |
| `sleep_time` | `int` | `1` | Interval between retries. |
| `reason` | `str \| None` | `None` | Expected condition reason. If set, must match exactly. |
| `message` | `str` | `""` | Expected substring within the condition message. |
| `stop_condition` | `str \| None` | `None` | Condition type that, if matched, stops the wait and fails immediately. |
| `stop_status` | `str` | `"True"` | Status for `stop_condition` matching. |

**Raises:**

| Exception | Condition |
|---|---|
| `TimeoutExpiredError` | Condition not met within timeout. |
| `ConditionError` | `stop_condition` is detected with matching `stop_status`. |

```python
ns.wait_for_condition(
    condition="Ready",
    status="True",
    timeout=60,
)
```

See [Waiting for Resource Conditions and Status](waiting-for-conditions.html) for more patterns.

#### `wait_for_conditions`

```python
def wait_for_conditions(self) -> None
```

Wait for the resource to have any conditions populated in its status. Uses a 30-second timeout.

---

### Serialization

#### `to_dict`

```python
def to_dict(self) -> None
```

Populate `self.res` with the intended dict representation of the resource. Called automatically before `create()`. Override this in subclasses to add resource-specific fields.

> **Note:** If `kind_dict` or `yaml_file` was provided, `to_dict()` uses those directly instead of building from individual parameters.

#### `to_yaml`

```python
def to_yaml(self) -> str
```

**Returns:** `str` — YAML string representation of the resource dict.

```python
print(ns.to_yaml())
```

---

### Watch / Events

#### `watcher`

```python
def watcher(
    self,
    timeout: int,
    resource_version: str = "",
) -> Generator[dict[str, Any], None, None]
```

Watch for changes to this specific resource.

| Parameter | Type | Default | Description |
|---|---|---|---|
| `timeout` | `int` | — | Watch duration in seconds. |
| `resource_version` | `str` | `""` | Only return events after this version. Defaults to the version at resource creation time. |

**Yields:** Event dicts with keys `type` (`"ADDED"`, `"MODIFIED"`, `"DELETED"`), `raw_object`, and `object`.

```python
for event in ns.watcher(timeout=30):
    print(event["type"], event["object"].metadata.name)
```

#### `events`

```python
def events(
    self,
    name: str = "",
    label_selector: str = "",
    field_selector: str = "",
    resource_version: str = "",
    timeout: int = 240,
) -> Generator[Any, Any, None]
```

Get Kubernetes events related to this resource.

| Parameter | Type | Default | Description |
|---|---|---|---|
| `name` | `str` | `""` | Filter by event name. |
| `label_selector` | `str` | `""` | Comma-separated `key=value` label filters. |
| `field_selector` | `str` | `""` | Additional field selectors (auto-prefixed with `involvedObject.name==<resource_name>`). |
| `resource_version` | `str` | `""` | Filter events by resource version. |
| `timeout` | `int` | `240` | Timeout in seconds. |

**Yields:** `Event` objects.

```python
for event in pod.events(field_selector="type==Warning", timeout=10):
    print(event.object)
```

---

### Validation

#### `validate`

```python
def validate(self) -> None
```

Validate `self.res` against the OpenAPI schema for this resource kind. Called automatically during `create()` and `update_replace()` when `schema_validation_enabled=True`.

**Raises:** `ValidationError` if the resource dict is invalid.

```python
pod = Pod(client=client, name="test", namespace="default")
pod.to_dict()
pod.validate()  # Raises ValidationError on invalid spec
```

#### `validate_dict` (classmethod)

```python
@classmethod
def validate_dict(cls, resource_dict: dict[str, Any]) -> None
```

Validate an arbitrary resource dictionary against the schema without creating a resource instance.

| Parameter | Type | Description |
|---|---|---|
| `resource_dict` | `dict[str, Any]` | Complete resource dictionary. |

**Raises:** `ValidationError` if validation fails.

```python
from ocp_resources.pod import Pod

Pod.validate_dict({
    "apiVersion": "v1",
    "kind": "Pod",
    "metadata": {"name": "test"},
    "spec": {"containers": [{"name": "web", "image": "nginx"}]},
})
```

See [Validating Resources Against OpenAPI Schemas](validating-resources.html) for full validation guide.

---

### API Request

#### `api_request`

```python
def api_request(
    self,
    method: str,
    action: str,
    url: str,
    retry_params: dict[str, int] | None = None,
    **params: Any,
) -> dict[str, Any]
```

Send a raw HTTP request to the resource's API endpoint. Used internally for subresource actions (e.g., VirtualMachine start/stop).

| Parameter | Type | Default | Description |
|---|---|---|---|
| `method` | `str` | — | HTTP method (`"GET"`, `"PUT"`, `"POST"`, etc.). |
| `action` | `str` | — | Subresource action to append to the URL (e.g., `"start"`, `"stop"`). |
| `url` | `str` | — | Base URL of the resource. |
| `retry_params` | `dict[str, int] \| None` | `None` | Dict with `timeout` and `sleep_time` keys for retry behavior. |
| `**params` | | | Additional params passed to the HTTP request. |

**Returns:** `dict[str, Any]` — Parsed JSON response, or raw response data if not valid JSON.

---

### Utility Methods

#### `retry_cluster_exceptions` (static)

```python
@staticmethod
def retry_cluster_exceptions(
    func: Callable,
    exceptions_dict: dict[type[Exception], list[str]] = DEFAULT_CLUSTER_RETRY_EXCEPTIONS,
    timeout: int = 10,
    sleep_time: int = 1,
    **kwargs: Any,
) -> Any
```

Retry a callable on transient cluster errors.

| Parameter | Type | Default | Description |
|---|---|---|---|
| `func` | `Callable` | — | Function to call. |
| `exceptions_dict` | `dict` | `DEFAULT_CLUSTER_RETRY_EXCEPTIONS` | Map of exception types to message substrings to match. |
| `timeout` | `int` | `10` | Total retry timeout in seconds. |
| `sleep_time` | `int` | `1` | Sleep between retries. |
| `**kwargs` | | | Passed to `func`. |

**Returns:** The return value of `func`.

**Raises:** The last exception if timeout is reached.

#### `get_condition_message`

```python
def get_condition_message(
    self,
    condition_type: str,
    condition_status: str = "",
) -> str
```

Get the message for a specific condition.

| Parameter | Type | Default | Description |
|---|---|---|---|
| `condition_type` | `str` | — | Condition type name. |
| `condition_status` | `str` | `""` | If set, only return the message when the condition status matches. |

**Returns:** `str` — The condition message, or `""` if not found or status doesn't match.

#### `hash_resource_dict`

```python
def hash_resource_dict(self, resource_dict: dict[Any, Any]) -> dict[Any, Any]
```

Replace sensitive fields (defined by `keys_to_hash`) with `"*******"` for logging.

| Parameter | Type | Description |
|---|---|---|
| `resource_dict` | `dict` | The resource dict to hash. |

**Returns:** `dict` — A copy with sensitive values replaced.

> **Tip:** Override the `keys_to_hash` property in subclasses to define which fields to mask.

#### `keys_to_hash`

```python
@property
def keys_to_hash(self) -> list[str]
```

List of key paths to mask in logs. Uses `>` as separator, `[]` for list elements.

**Returns:** `list[str]` — Default: `[]` (no hashing).

```python
# Example override in a Secret subclass:
@property
def keys_to_hash(self):
    return ["data", "stringData"]
```

---

### Inner Classes

#### `Resource.Status`

Pre-defined status phase constants (inherited from `ResourceConstants`).

| Constant | Value |
|---|---|
| `Status.SUCCEEDED` | `"Succeeded"` |
| `Status.FAILED` | `"Failed"` |
| `Status.DELETING` | `"Deleting"` |
| `Status.DEPLOYED` | `"Deployed"` |
| `Status.PENDING` | `"Pending"` |
| `Status.COMPLETED` | `"Completed"` |
| `Status.RUNNING` | `"Running"` |
| `Status.READY` | `"Ready"` |
| `Status.TERMINATING` | `"Terminating"` |
| `Status.ERROR` | `"Error"` |
| `Status.ACTIVE` | `"Active"` |
| `Status.SCHEDULING` | `"Scheduling"` |
| `Status.CRASH_LOOPBACK_OFF` | `"CrashLoopBackOff"` |
| `Status.IMAGE_PULL_BACK_OFF` | `"ImagePullBackOff"` |

```python
pod.wait_for_status(status=Pod.Status.RUNNING)
```

#### `Resource.Condition`

Pre-defined condition type and status constants.

| Constant | Value |
|---|---|
| `Condition.READY` | `"Ready"` |
| `Condition.AVAILABLE` | `"Available"` |
| `Condition.DEGRADED` | `"Degraded"` |
| `Condition.PROGRESSING` | `"Progressing"` |
| `Condition.UPGRADEABLE` | `"Upgradeable"` |
| `Condition.Status.TRUE` | `"True"` |
| `Condition.Status.FALSE` | `"False"` |
| `Condition.Status.UNKNOWN` | `"Unknown"` |

```python
ns.wait_for_condition(
    condition=Resource.Condition.READY,
    status=Resource.Condition.Status.TRUE,
)
```

#### `Resource.ApiGroup`

Pre-defined API group string constants. A selection of commonly used values:

| Constant | Value |
|---|---|
| `ApiGroup.APPS` | `"apps"` |
| `ApiGroup.BATCH` | `"batch"` |
| `ApiGroup.NETWORKING_K8S_IO` | `"networking.k8s.io"` |
| `ApiGroup.RBAC_AUTHORIZATION_K8S_IO` | `"rbac.authorization.k8s.io"` |
| `ApiGroup.STORAGE_K8S_IO` | `"storage.k8s.io"` |
| `ApiGroup.CONFIG_OPENSHIFT_IO` | `"config.openshift.io"` |
| `ApiGroup.KUBEVIRT_IO` | `"kubevirt.io"` |
| `ApiGroup.CDI_KUBEVIRT_IO` | `"cdi.kubevirt.io"` |
| `ApiGroup.ROUTE_OPENSHIFT_IO` | `"route.openshift.io"` |
| `ApiGroup.MACHINE_OPENSHIFT_IO` | `"machine.openshift.io"` |

> **Note:** Over 100 API group constants are available. Use IDE auto-complete to discover all values.

#### `Resource.ApiVersion`

| Constant | Value |
|---|---|
| `ApiVersion.V1` | `"v1"` |
| `ApiVersion.V1BETA1` | `"v1beta1"` |
| `ApiVersion.V1ALPHA1` | `"v1alpha1"` |
| `ApiVersion.V1ALPHA3` | `"v1alpha3"` |

---

## `NamespacedResource`

```python
from ocp_resources.resource import NamespacedResource
```

Base class for **namespace-scoped** resources (e.g., `Pod`, `Deployment`, `Service`, `ConfigMap`). Extends `Resource` with namespace awareness.

### Constructor

```python
NamespacedResource(
    client=client,
    name="my-pod",
    namespace="my-namespace",
)
```

All parameters from [`Resource`](#constructor) are accepted, plus:

| Parameter | Type | Default | Description |
|---|---|---|---|
| `namespace` | `str \| None` | `None` | Kubernetes namespace. Required unless `yaml_file` or `kind_dict` is provided. |

**Raises:** `MissingRequiredArgumentError` if neither (`name` + `namespace`) nor `yaml_file` / `kind_dict` is provided.

```python
from ocp_resources.pod import Pod

pod = Pod(
    client=client,
    name="nginx",
    namespace="default",
    label={"app": "web"},
)
```

### Overridden Methods

#### `instance`

```python
@property
def instance(self) -> ResourceInstance
```

Get the live resource instance, scoped to `self.namespace`.

**Returns:** `ResourceInstance`

#### `to_dict`

```python
def to_dict(self) -> None
```

Populates `self.res` and sets `metadata.namespace`. If using `yaml_file` or `kind_dict`, reads the namespace from the YAML/dict.

**Raises:** `MissingRequiredArgumentError` if namespace is still not set after processing.

#### `get` (classmethod)

Behaves like `Resource.get()`, but yields instances constructed with both `name` and `namespace`.

```python
from ocp_resources.pod import Pod

for pod in Pod.get(client=client, namespace="default"):
    print(f"{pod.namespace}/{pod.name}")

# With label selector
for pod in Pod.get(client=client, namespace="default", label_selector="app=web"):
    print(pod.name)
```

---

## `ResourceEditor`

```python
from ocp_resources.resource import ResourceEditor
```

Temporarily patch resources and automatically restore original values. Designed for test scenarios.

### Constructor

```python
ResourceEditor(
    patches: dict[Any, Any],
    action: str = "update",
    user_backups: dict[Any, Any] | None = None,
)
```

| Parameter | Type | Default | Description |
|---|---|---|---|
| `patches` | `dict` | — | Map of `{Resource: patch_dict}`. |
| `action` | `str` | `"update"` | `"update"` for merge-patch, `"replace"` for full replacement. |
| `user_backups` | `dict \| None` | `None` | Pre-computed backup dicts. If provided, skips automatic backup creation. |

### Context Manager Usage

```python
from ocp_resources.resource import ResourceEditor
from ocp_resources.namespace import Namespace

ns = Namespace(client=client, name="my-ns", ensure_exists=True)

with ResourceEditor(
    patches={ns: {"metadata": {"labels": {"temporary": "true"}}}}
) as editor:
    # Labels are patched
    assert ns.instance.metadata.labels.temporary == "true"
# Labels are restored to original values
```

### Methods

| Method | Signature | Description |
|---|---|---|
| `update` | `update(backup_resources: bool = False) -> None` | Apply patches. If `backup_resources=True`, back up original values first. |
| `restore` | `restore() -> None` | Restore all backed-up values. |

### Properties

| Property | Type | Description |
|---|---|---|
| `backups` | `dict[Any, Any]` | Backed-up original values for each patched resource. |
| `patches` | `dict[Any, Any]` | The patches dict from the constructor. |

> **Warning:** The `DynamicClient` used to obtain the resource objects must have sufficient privileges to patch and replace resources.

See [Editing Resources Temporarily with ResourceEditor](editing-resources-temporarily.html) for detailed patterns.

---

## `ResourceList`

```python
from ocp_resources.resource import ResourceList
```

Create and manage N copies of a cluster-scoped resource with indexed names.

### Constructor

```python
ResourceList(
    resource_class: type[Resource],
    num_resources: int,
    client: DynamicClient,
    **kwargs: Any,
)
```

| Parameter | Type | Description |
|---|---|---|
| `resource_class` | `type[Resource]` | The resource class to instantiate. |
| `num_resources` | `int` | Number of resource copies to create. |
| `client` | `DynamicClient` | Kubernetes client. |
| `**kwargs` | | Passed to each resource constructor. Must include `name` (used as base name). |

Resources are named `{name}-1`, `{name}-2`, ..., `{name}-N`.

```python
from ocp_resources.namespace import Namespace
from ocp_resources.resource import ResourceList

namespaces = ResourceList(
    resource_class=Namespace,
    num_resources=3,
    client=client,
    name="test-ns",
)

with namespaces:
    # Creates test-ns-1, test-ns-2, test-ns-3
    for ns in namespaces:
        print(ns.name)
# All three namespaces are deleted
```

### Methods (inherited from `BaseResourceList`)

| Method | Signature | Returns | Description |
|---|---|---|---|
| `deploy` | `deploy(wait=False)` | `list[Resource]` | Deploy all resources. |
| `clean_up` | `clean_up(wait=True)` | `bool` | Delete all resources in reverse order. |
| `__len__` | `__len__()` | `int` | Number of resources. |
| `__getitem__` | `__getitem__(index)` | `Resource` | Access by index. |
| `__iter__` | `__iter__()` | `Generator` | Iterate over resources. |

---

## `NamespacedResourceList`

```python
from ocp_resources.resource import NamespacedResourceList
```

Create one instance of a namespaced resource in each namespace from a `ResourceList`.

### Constructor

```python
NamespacedResourceList(
    resource_class: type[NamespacedResource],
    namespaces: ResourceList,
    client: DynamicClient,
    **kwargs: Any,
)
```

| Parameter | Type | Description |
|---|---|---|
| `resource_class` | `type[NamespacedResource]` | The namespaced resource class (e.g., `Pod`). |
| `namespaces` | `ResourceList` | A `ResourceList` of `Namespace` resources. |
| `client` | `DynamicClient` | Kubernetes client. |
| `**kwargs` | | Passed to each resource constructor. Must include `name`. |

**Raises:** `TypeError` if any resource in `namespaces` is not a `Namespace`.

```python
from ocp_resources.namespace import Namespace
from ocp_resources.pod import Pod
from ocp_resources.resource import ResourceList, NamespacedResourceList

namespaces = ResourceList(
    resource_class=Namespace, num_resources=2, client=client, name="test-ns"
)

pods = NamespacedResourceList(
    resource_class=Pod,
    namespaces=namespaces,
    client=client,
    name="nginx",
)

with namespaces:
    with pods:
        # Creates nginx in test-ns-1 and test-ns-2
        for pod in pods:
            print(f"{pod.namespace}/{pod.name}")
```

See [Managing Bulk Resources with ResourceList](managing-resource-lists.html) for full usage patterns.

---

## `KubeAPIVersion`

```python
from ocp_resources.resource import KubeAPIVersion
```

Implements [Kubernetes API versioning](https://kubernetes.io/docs/concepts/overview/kubernetes-api/#api-versioning) with comparison operators. Extends `packaging.version.Version`.

### Constructor

```python
KubeAPIVersion(vstring: str)
```

| Parameter | Type | Description |
|---|---|---|
| `vstring` | `str` | Version string (e.g., `"v1"`, `"v1beta1"`, `"v1alpha2"`). |

**Raises:** `ValueError` if the version string does not conform to Kubernetes versioning.

### Ordering

Versions are ordered: `v1alpha1 < v1alpha2 < v1beta1 < v1beta2 < v1 < v2`.

```python
assert KubeAPIVersion("v1") > KubeAPIVersion("v1beta1")
assert KubeAPIVersion("v1beta1") > KubeAPIVersion("v1alpha1")
assert KubeAPIVersion("v1") == KubeAPIVersion("v1")
```

---

## Exceptions

All exceptions are importable from `ocp_resources.exceptions`.

```python
from ocp_resources.exceptions import (
    MissingRequiredArgumentError,
    ResourceTeardownError,
    ValidationError,
    ConditionError,
)
```

| Exception | Raised By | Description |
|---|---|---|
| `MissingRequiredArgumentError` | `Resource.__init__`, `NamespacedResource.__init__`, `NamespacedResource._base_body` | Required parameters (`name`, `namespace`) not provided. |
| `ResourceTeardownError` | `Resource.__exit__` | `clean_up()` returned `False` during context manager exit. |
| `ValidationError` | `validate()`, `validate_dict()`, `create()`, `update_replace()` | Resource dict fails OpenAPI schema validation. Has `message`, `path`, and `schema_error` attributes. |
| `ConditionError` | `wait_for_condition()` | A `stop_condition` was detected during condition waiting. |
| `MissingResourceResError` | `Resource._base_body` | Deprecated. `self.res` is empty after `_base_body()`. |

---

## Timeout Constants

Available from `ocp_resources.utils.constants`:

```python
from ocp_resources.utils.constants import (
    TIMEOUT_1SEC,      # 1
    TIMEOUT_5SEC,      # 5
    TIMEOUT_10SEC,     # 10
    TIMEOUT_30SEC,     # 30
    TIMEOUT_1MINUTE,   # 60
    TIMEOUT_2MINUTES,  # 120
    TIMEOUT_4MINUTES,  # 240
    TIMEOUT_10MINUTES, # 600
)
```

---

## Module-Level Functions

### `replace_key_with_hashed_value`

```python
from ocp_resources.resource import replace_key_with_hashed_value

replace_key_with_hashed_value(resource_dict: dict, key_name: str) -> dict
```

Recursively replace the value at a key path with `"*******"`. Supports dict paths (`a>b>c`) and list paths (`a>b[]>c`).

| Parameter | Type | Description |
|---|---|---|
| `resource_dict` | `dict` | The nested dictionary. |
| `key_name` | `str` | Key path using `>` as separator, `[]` for list elements. |

**Returns:** `dict` — A deep copy with the value replaced.

```python
data = {"spec": {"secret": {"token": "sensitive"}}}
hashed = replace_key_with_hashed_value(data, "spec>secret>token")
# {"spec": {"secret": {"token": "*******"}}}

# With list path
data = {"spec": {"containers": [{"env": [{"value": "secret"}]}]}}
hashed = replace_key_with_hashed_value(data, "spec>containers[]>env[]>value")
```

## Related Pages

- [Understanding the Resource Class Hierarchy](resource-class-hierarchy.html)
- [Creating and Managing Resources](creating-and-managing-resources.html)
- [Querying and Watching Resources](querying-resources.html)
- [Waiting for Resource Conditions and Status](waiting-for-conditions.html)
- [Environment Variables and Configuration](environment-variables.html)

---

Source: class-generator-cli.md

I'll start by reading the knowledge graph and page manifest, then explore the class-generator source code in detail.

Now let me check the entry point configuration and any additional schema-related functions:

Now let me check the test files for CLI tests and the entry point:

Now let me check the parsers and any additional CLI-relevant components:

Now I have all the information I need. Let me compile the comprehensive CLI reference page.

# class-generator CLI Reference

The `class-generator` command generates Python wrapper classes for Kubernetes and OpenShift resources. It is installed as a console script entry point from the `openshift-python-wrapper` package.

```
class-generator = "class_generator.cli:main"
```

```bash
pip install openshift-python-wrapper
# or
uv tool install openshift-python-wrapper
```

> **Tip:** Enable shell completion by adding to `~/.bashrc` or `~/.zshrc`:
> ```bash
> if type class-generator > /dev/null; then eval "$(_CLASS_GENERATOR_COMPLETE=zsh_source class-generator)"; fi
> ```

## Synopsis

```
class-generator [OPTIONS]
```

At least one action option must be specified: `--kind`, `--update-schema`, `--update-schema-for`, `--discover-missing`, `--coverage-report`, `--generate-missing`, or `--regenerate-all`.

---

## Options Reference

### Kind Generation

#### `-k`, `--kind`

| Property | Value |
|----------|-------|
| **Type** | `STRING` |
| **Default** | `None` |
| **Required** | No (but at least one action must be specified) |
| **Requires** | Connected cluster with admin privileges |

Generate Python wrapper classes for the specified Kubernetes resource Kind(s). Multiple kinds can be comma-separated (no spaces).

When a kind is not found in the local schema mapping file, the CLI interactively prompts to run `--update-schema` (only in interactive CLI mode).

```bash
# Single kind
class-generator -k Pod

# Multiple kinds (processed in parallel)
class-generator -k Deployment,Pod,ConfigMap
```

When multiple kinds share the same Kind name but belong to different API groups (e.g., `DNS` from `config.openshift.io` and `operator.openshift.io`), separate files are generated with API group suffixes:

```
dns_config_openshift_io.py
dns_operator_openshift_io.py
```

---

#### `-o`, `--output-file`

| Property | Value |
|----------|-------|
| **Type** | `PATH` |
| **Default** | `ocp_resources/<snake_case_kind>.py` |
| **Required** | No |

Override the output file path for the generated Python module. If not provided, the filename is derived from the Kind using `convert_camel_case_to_snake_case`.

```bash
class-generator -k Pod -o my_custom_pod.py
```

> **Note:** When generating multiple comma-separated kinds, the `--output-file` value applies to all kinds. For independent output paths, run separate commands.

---

#### `--overwrite`

| Property | Value |
|----------|-------|
| **Type** | Flag |
| **Default** | `False` |

Overwrite an existing output file. Without this flag, if the target file already exists, a `_TEMP.py` suffixed file is created instead.

When overwriting, any user-added code blocks (code after the `# End of generated code` marker) and user imports are preserved in the regenerated file.

```bash
class-generator -k Pod --overwrite
```

---

#### `--dry-run`

| Property | Value |
|----------|-------|
| **Type** | Flag |
| **Default** | `False` |

Preview the generated output without writing any files. The generated Python code is printed to the console with syntax highlighting and line numbers using Rich.

```bash
class-generator -k Pod --dry-run
```

---

#### `--backup`

| Property | Value |
|----------|-------|
| **Type** | Flag |
| **Default** | `False` |
| **Requires** | `--regenerate-all` or `--overwrite` |

Create a timestamped backup of existing files before overwriting or regenerating. Backups are stored in `.backups/backup-YYYYMMDD-HHMMSS/` preserving the original directory structure.

```bash
class-generator -k Pod --overwrite --backup
```

> **Note:** Using `--backup` without either `--regenerate-all` or `--overwrite` results in a constraint error.

---

### Test Generation

#### `--add-tests`

| Property | Value |
|----------|-------|
| **Type** | Flag |
| **Default** | `False` |
| **Requires** | `-k`/`--kind` |

Generate test files for the specified Kind and run them. This performs two actions:

1. Generates a test manifest in `class_generator/tests/manifests/<Kind>/` and regenerates `class_generator/tests/test_class_generator.py` from the Jinja2 template.
2. Runs the generated test file using `uv run --group tests pytest class_generator/tests/test_class_generator.py`.

```bash
class-generator -k Pod --add-tests
```

> **Warning:** `--add-tests` cannot be used without `-k`/`--kind`. Running `class-generator --add-tests` alone exits with a non-zero status.

---

### Schema Management

#### `--update-schema`

| Property | Value |
|----------|-------|
| **Type** | Flag |
| **Default** | `False` |
| **Requires** | Connected cluster; `oc` or `kubectl` in PATH |
| **Mutually exclusive with** | `--update-schema-for` |

Fetch all resource schemas from the connected cluster's OpenAPI v3 endpoints and update the local schema files:

- `class_generator/schema/__resources-mappings.json` (compressed as `.json.gz`)
- `class_generator/schema/_definitions.json`

The update strategy depends on the cluster version:

| Cluster Version | Behavior |
|-----------------|----------|
| Same or newer than last update | Full update — fetches all schemas, updates existing resources |
| Older than last update | Incremental — only adds missing resources, preserves existing schemas |

When used alone, exits after updating. When combined with `--generate-missing`, continues to resource generation after the update.

```bash
# Update schema only
class-generator --update-schema

# Update schema then generate missing resources
class-generator --update-schema --generate-missing
```

> **Note:** When `--update-schema` is used without `--generate-missing`, it cannot be combined with `-k`, `--discover-missing`, `--coverage-report`, `--dry-run`, `--overwrite`, `-o`, `--add-tests`, or `--regenerate-all`.

---

#### `--update-schema-for`

| Property | Value |
|----------|-------|
| **Type** | `STRING` |
| **Default** | `None` |
| **Requires** | Connected cluster; `oc` or `kubectl` in PATH |
| **Mutually exclusive with** | `--update-schema` |

Update the schema for a single resource Kind without affecting other resources. The Kind name is **case-sensitive**.

This fetches only the API paths relevant to the specified Kind, updates (or adds) its schema in the mapping file, and exits.

```bash
class-generator --update-schema-for LlamaStackDistribution
```

Use cases:
- Connected to an older cluster but need to update a specific CRD
- A new operator was installed and you need its resource schema
- Refreshing just one resource without a full schema update

After updating, regenerate the class:

```bash
class-generator --update-schema-for LlamaStackDistribution
class-generator -k LlamaStackDistribution --overwrite
```

**Errors:**

| Error | Cause |
|-------|-------|
| `ResourceNotFoundError` | The Kind is not found on the cluster (CRD not installed or name misspelled) |
| `RuntimeError` | API paths not found or schema extraction failed |

> **Note:** Cannot be combined with `-k`, `--discover-missing`, `--coverage-report`, `--generate-missing`, or `--regenerate-all`.

---

### Coverage and Discovery

#### `--discover-missing`

| Property | Value |
|----------|-------|
| **Type** | Flag |
| **Default** | `False` |

Analyze resource coverage by comparing schema-mapped resources against implemented wrapper classes in `ocp_resources/`. Generates a console report showing coverage statistics and missing resources.

```bash
class-generator --discover-missing
```

---

#### `--coverage-report`

| Property | Value |
|----------|-------|
| **Type** | Flag |
| **Default** | `False` |

Generate a detailed coverage report showing:

| Metric | Description |
|--------|-------------|
| Total Resources in Schema | Number of resource Kinds in the schema mapping |
| Auto-Generated Resources | Wrapper classes with the generated marker |
| Coverage | Percentage of mapped resources with generated classes |
| Missing (Not Generated) | Resources in schema but without generated classes |
| Manual Implementations | Resource classes without the generated marker |

```bash
# Console output (default)
class-generator --coverage-report

# JSON output
class-generator --coverage-report --json
```

---

#### `--json`

| Property | Value |
|----------|-------|
| **Type** | Flag |
| **Default** | `False` |

Output reports in JSON format instead of Rich console tables. Applies to `--coverage-report`, `--discover-missing`, and `--generate-missing`.

The JSON output structure:

```json
{
  "generated_resources": ["ConfigMap", "Deployment", "Pod"],
  "manual_resources": ["VirtualMachine"],
  "missing_resources": [{"kind": "Binding"}, {"kind": "ComponentStatus"}],
  "coverage_stats": {
    "total_in_mapping": 400,
    "total_generated": 197,
    "total_manual": 25,
    "coverage_percentage": 49.25,
    "missing_count": 203
  }
}
```

```bash
class-generator --coverage-report --json
```

---

#### `--generate-missing`

| Property | Value |
|----------|-------|
| **Type** | Flag |
| **Default** | `False` |

Generate wrapper classes for all resources found in the schema mapping that do not yet have generated files. Each missing resource Kind is passed to `class_generator()` individually.

Can be combined with `--update-schema` to first refresh the schema, then generate all missing classes.

```bash
# Generate missing resources
class-generator --generate-missing

# Update schema first, then generate missing
class-generator --update-schema --generate-missing

# Dry run to preview
class-generator --generate-missing --dry-run
```

---

### Batch Regeneration

#### `--regenerate-all`

| Property | Value |
|----------|-------|
| **Type** | Flag |
| **Default** | `False` |

Regenerate all existing generated resource classes using the latest schemas. Only files containing the `# Generated using` marker in `ocp_resources/` are processed.

Regeneration runs in parallel (up to 10 workers). User-added code (below `# End of generated code`) is preserved during regeneration.

```bash
# Regenerate all resources
class-generator --regenerate-all

# With backup
class-generator --regenerate-all --backup

# Dry run
class-generator --regenerate-all --dry-run

# Filter to specific resources
class-generator --regenerate-all --filter "Pod*"
```

Output summary:

```
Regeneration complete: 195 succeeded, 2 failed
Backup files stored in: .backups/backup-20260705-143022
```

---

#### `--filter`

| Property | Value |
|----------|-------|
| **Type** | `STRING` |
| **Default** | `None` |
| **Requires** | `--regenerate-all` |

Filter which resources to regenerate using a glob pattern matched against the resource Kind name. Uses `fnmatch` semantics.

```bash
# Regenerate only Pod-related resources
class-generator --regenerate-all --filter "Pod*"

# Regenerate resources ending in "Service"
class-generator --regenerate-all --filter "*Service"

# Regenerate a specific resource
class-generator --regenerate-all --filter "VirtualMachine"
```

---

### Logging

#### `-v`, `--verbose`

| Property | Value |
|----------|-------|
| **Type** | Flag |
| **Default** | `False` |

Enable verbose output with debug-level logs. Sets `DEBUG` level on the following loggers:

- `class_generator.core.schema`
- `class_generator.core.generator`
- `class_generator.core.coverage`
- `class_generator.core.discovery`
- `class_generator.cli`
- `class_generator.utils`
- `ocp_resources`

```bash
class-generator -k Pod -v
```

---

## Constraint Rules

The CLI enforces the following option constraints:

| Constraint | Rule |
|------------|------|
| `--update-schema` ↔ `--update-schema-for` | Mutually exclusive |
| `--update-schema` (alone) | Cannot combine with `-k`, `--discover-missing`, `--coverage-report`, `--dry-run`, `--overwrite`, `-o`, `--add-tests`, `--regenerate-all` |
| `--update-schema-for` | Cannot combine with `-k`, `--discover-missing`, `--coverage-report`, `--generate-missing`, `--regenerate-all` |
| `--backup` | Requires `--regenerate-all` or `--overwrite` |
| `--filter` | Requires `--regenerate-all` |
| No options | Exits with error — at least one action required |

---

## Execution Order

When multiple compatible options are specified together, the CLI processes them in this fixed order:

1. `--update-schema-for` (exits after completion)
2. `--update-schema` (exits unless `--generate-missing` is also set)
3. `--coverage-report` / `--discover-missing` / `--generate-missing` (coverage analysis and reporting)
4. `--generate-missing` (generates classes for missing resources)
5. `--regenerate-all` (batch regeneration, exits after completion)
6. `-k`/`--kind` (normal kind generation)
7. `--add-tests` (test generation and execution)

---

## Exit Codes

| Code | Meaning |
|------|---------|
| `0` | Success |
| `1` | Generation failure, schema update failure, resource not found, or any kind in a batch failed |
| `2` | Invalid CLI arguments or constraint violation |

---

## Programmatic API

The generation logic can be invoked directly from Python. See [Generating New Resource Classes with class-generator](generating-resource-classes.html) for usage examples.

### `class_generator.core.generator.class_generator()`

```python
from class_generator.core.generator import class_generator

generated_files: list[str] = class_generator(
    kind="Pod",
    overwrite=False,
    dry_run=False,
    output_file="",
    output_dir="",
    add_tests=False,
    called_from_cli=True,
    update_schema_executed=False,
)
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `kind` | `str` | *(required)* | Kubernetes resource Kind |
| `overwrite` | `bool` | `False` | Overwrite existing files |
| `dry_run` | `bool` | `False` | Preview output without writing files |
| `output_file` | `str` | `""` | Specific output file path |
| `output_dir` | `str` | `""` | Output directory (defaults to `ocp_resources`) |
| `add_tests` | `bool` | `False` | Generate test files |
| `called_from_cli` | `bool` | `True` | Enables interactive prompts when `True` |
| `update_schema_executed` | `bool` | `False` | Whether schema update was already performed |

**Returns:** `list[str]` — List of generated file paths. Empty list if the kind is not found in the schema mapping (when `called_from_cli=False`).

**Raises:**
- `RuntimeError` — Kind not found after schema update, or user declined schema update
- `ValueError` — Generated filename contains invalid patterns (single-letter segments)

---

### `class_generator.core.generator.generate_resource_file_from_dict()`

```python
from class_generator.core.generator import generate_resource_file_from_dict

orig_filename, generated_filename = generate_resource_file_from_dict(
    resource_dict={"kind": "Pod", ...},
    overwrite=False,
    dry_run=False,
    output_file="",
    add_tests=False,
    output_file_suffix="",
    output_dir="",
)
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `resource_dict` | `dict[str, Any]` | *(required)* | Dictionary containing parsed resource information |
| `overwrite` | `bool` | `False` | Overwrite existing files |
| `dry_run` | `bool` | `False` | Preview without writing |
| `output_file` | `str` | `""` | Specific output file path |
| `add_tests` | `bool` | `False` | Generate test files under `class_generator/tests/manifests/` |
| `output_file_suffix` | `str` | `""` | Suffix appended to filename (for API group disambiguation) |
| `output_dir` | `str` | `""` | Output directory (defaults to `ocp_resources`) |

**Returns:** `tuple[str, str]` — `(original_filename, generated_filename)`. These differ when a `_TEMP.py` file is created.

---

### `class_generator.core.schema.update_kind_schema()`

```python
from class_generator.core.schema import update_kind_schema

update_kind_schema(client=None)
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `client` | `str \| None` | `None` | Path to `oc`/`kubectl` binary. Auto-detected if `None`. |

**Raises:**
- `ClusterVersionError` — Cannot determine cluster version
- `RuntimeError` — Failed to fetch OpenAPI v3 index
- `OSError` — Failed to write schema files

---

### `class_generator.core.schema.update_single_resource_schema()`

```python
from class_generator.core.schema import update_single_resource_schema

update_single_resource_schema(kind="LlamaStackDistribution", client=None)
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `kind` | `str` | *(required)* | Resource Kind name (case-sensitive) |
| `client` | `str \| None` | `None` | Path to `oc`/`kubectl` binary. Auto-detected if `None`. |

**Raises:**
- `ResourceNotFoundError` — Kind not found on the cluster
- `RuntimeError` — Schema extraction or save failed

---

### `class_generator.core.coverage.analyze_coverage()`

```python
from class_generator.core.coverage import analyze_coverage

result: dict[str, Any] = analyze_coverage(resources_dir="ocp_resources")
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `resources_dir` | `str` | `"ocp_resources"` | Directory to scan for wrapper classes |

**Returns:** `dict[str, Any]` with keys:

| Key | Type | Description |
|-----|------|-------------|
| `generated_resources` | `list[str]` | Sorted list of auto-generated resource class names |
| `manual_resources` | `list[str]` | Sorted list of manually written resource class names |
| `missing_resources` | `list[dict]` | List of `{"kind": "..."}` for resources in schema but not generated |
| `coverage_stats` | `dict` | Statistics including `total_in_mapping`, `total_generated`, `total_manual`, `coverage_percentage`, `missing_count` |

---

### `class_generator.core.discovery.discover_generated_resources()`

```python
from class_generator.core.discovery import discover_generated_resources

resources: list[dict[str, Any]] = discover_generated_resources()
```

**Returns:** `list[dict[str, Any]]` — Each dict contains:

| Key | Type | Description |
|-----|------|-------------|
| `path` | `str` | Full path to the resource file |
| `kind` | `str` | Resource class name |
| `filename` | `str` | File name without extension |
| `has_user_code` | `bool` | Whether file contains user modifications below `# End of generated code` |

---

### `class_generator.core.discovery.discover_cluster_resources()`

```python
from class_generator.core.discovery import discover_cluster_resources

resources: dict[str, list[dict[str, Any]]] = discover_cluster_resources(
    client=None,
    api_group_filter=None,
)
```

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `client` | `DynamicClient \| None` | `None` | Kubernetes dynamic client. Creates one if `None`. |
| `api_group_filter` | `str \| None` | `None` | Filter by API group name |

**Returns:** `dict[str, list[dict[str, Any]]]` — Mapping of API version to list of resource dicts (`name`, `kind`, `namespaced`).

**Raises:** `ValueError` — If client is not a `DynamicClient` instance.

---

## Exceptions

### `class_generator.exceptions.ResourceNotFoundError`

```python
from class_generator.exceptions import ResourceNotFoundError
```

Raised when a resource Kind is not found in the schema mapping or on the cluster.

| Attribute | Type | Description |
|-----------|------|-------------|
| `kind` | `str` | The Kind that was not found |

### `class_generator.core.schema.ClusterVersionError`

```python
from class_generator.core.schema import ClusterVersionError
```

Raised when the cluster version cannot be determined (client binary missing, cluster unreachable, or authentication failure).

---

## Schema Files

The CLI manages two schema files under `class_generator/schema/`:

| File | Purpose |
|------|---------|
| `__resources-mappings.json.gz` | Compressed JSON mapping of lowercase Kind → list of schemas with GVK metadata and namespaced status |
| `_definitions.json` | JSON Schema definitions for `$ref` resolution during validation |
| `__cluster_version__.txt` | Last cluster version used for schema generation |

See [Schema Validation and Code Generation Architecture](schema-validation-internals.html) for details on how these files are structured and consumed.

---

## Common Workflows

### Generate a new resource class

```bash
class-generator -k MyCustomResource
```

### Update schema and regenerate all classes

```bash
class-generator --update-schema
class-generator --regenerate-all --backup
```

### Add a new CRD to an older cluster

```bash
class-generator --update-schema-for MyNewCRD
class-generator -k MyNewCRD
```

### CI/CD coverage check

```bash
class-generator --coverage-report --json > coverage.json
```

### Preview changes before writing

```bash
class-generator -k Pod --dry-run
class-generator --regenerate-all --dry-run
```

---

## Related Pages

- [Generating New Resource Classes with class-generator](generating-resource-classes.html) — Step-by-step guide for scaffolding resource classes
- [Resource and NamespacedResource API](resource-api.html) — API reference for the base classes that generated code extends
- [Understanding the Resource Class Hierarchy](resource-class-hierarchy.html) — How generated subclasses fit into the class hierarchy
- [Schema Validation and Code Generation Architecture](schema-validation-internals.html) — Internals of schema fetching, caching, and code generation
- [Environment Variables and Configuration](environment-variables.html) — Environment variables that affect runtime behavior

## Related Pages

- [Generating New Resource Classes with class-generator](generating-resource-classes.html)
- [Schema Validation and Code Generation Architecture](schema-validation-internals.html)
- [Understanding the Resource Class Hierarchy](resource-class-hierarchy.html)
- [Resource and NamespacedResource API](resource-api.html)
- [Environment Variables and Configuration](environment-variables.html)

---

Source: environment-variables.md

Let me start by reading the knowledge graph and page manifest, then explore the codebase for environment variables.

Now let me look at the `keys_to_hash` and hash-related functionality more closely:

Now let me look at the `get_logger` import and how log levels work:

Now let me check for any CI/CD or config-related environment variables:

Let me also look for any validation-related environment variables and enable_validation_by_default:

Now let me look at the `get_logger` usage and the `simple_logger` format:

Now let me check the deploy() and clean_up() methods more carefully, and also the hash_resource_dict method:

Let me also check how `replace_key_with_hashed_value` is defined:

Now let me check the MCP server for environment variables:

Now let me check if there are any other environment variable usages in the class_generator:

Let me verify there are no other env vars in tests:

Let me also check the full `get_client` function and the `save_kubeconfig` function:

Now let me look for any additional environment variables in fake_kubernetes_client:

Let me look at how the deploy and clean_up methods use the skip logic:

Now I have all the information needed. Let me write the documentation:

# Environment Variables and Configuration

This page documents all environment variables recognized by `openshift-python-wrapper` at runtime. These variables control logging behavior, resource lifecycle operations, proxy configuration, cluster connectivity, and sensitive data hashing.

## Quick Reference

| Environment Variable | Purpose | Default |
|---|---|---|
| `KUBECONFIG` | Path to kubeconfig file | `~/.kube/config` |
| `HTTPS_PROXY` | HTTPS proxy URL for cluster connections | _(unset)_ |
| `HTTP_PROXY` | HTTP proxy URL for cluster connections | _(unset)_ |
| `OPENSHIFT_PYTHON_WRAPPER_LOG_LEVEL` | Logging verbosity level | `INFO` |
| `OPENSHIFT_PYTHON_WRAPPER_LOG_FILE` | Path to log output file | `""` (stdout) |
| `OPENSHIFT_PYTHON_WRAPPER_HASH_LOG_DATA` | Enable/disable hashing sensitive data in logs | `true` |
| `REUSE_IF_RESOURCE_EXISTS` | Skip resource creation if already exists | _(unset)_ |
| `SKIP_RESOURCE_TEARDOWN` | Skip resource deletion during teardown | _(unset)_ |

---

## Cluster Connection

### `KUBECONFIG`

| Property | Value |
|---|---|
| **Type** | `str` |
| **Default** | `~/.kube/config` |
| **Used by** | `get_client()` |
| **Source** | `ocp_resources/resource.py` |

Path to the kubeconfig file used when creating a Kubernetes `DynamicClient`. Read when no explicit `config_file`, `config_dict`, `host`/`token`, or `username`/`password` arguments are passed to `get_client()`.

> **Note:** The standard `kubernetes` Python client reads `KUBECONFIG` at import time. If you set this variable in code (after import), `openshift-python-wrapper` handles it by explicitly passing the value to the client constructor. See [Connecting to Clusters](connecting-to-clusters.html) for all connection options.

```bash
export KUBECONFIG=/path/to/my/kubeconfig
```

```python
from ocp_resources.resource import get_client

# Automatically uses $KUBECONFIG, or falls back to ~/.kube/config
client = get_client()
```

---

### `HTTPS_PROXY` / `HTTP_PROXY`

| Property | Value |
|---|---|
| **Type** | `str` |
| **Default** | _(unset — no proxy)_ |
| **Used by** | `get_client()` |
| **Source** | `ocp_resources/resource.py` |

Sets the proxy on the Kubernetes client configuration when no proxy is already configured on the `client_configuration` object. `HTTPS_PROXY` takes precedence over `HTTP_PROXY` when both are set.

```bash
export HTTPS_PROXY=http://proxy.example.com:8080
```

```python
from ocp_resources.resource import get_client

# Proxy is automatically applied from environment
client = get_client()
```

> **Tip:** If you pass a `client_configuration` object that already has a `.proxy` set, the environment variables are ignored.

**Precedence order:**

1. Explicit `client_configuration.proxy` (if already set)
2. `HTTPS_PROXY` environment variable
3. `HTTP_PROXY` environment variable

---

## Logging

### `OPENSHIFT_PYTHON_WRAPPER_LOG_LEVEL`

| Property | Value |
|---|---|
| **Type** | `str` |
| **Default** | `INFO` |
| **Valid values** | `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` |
| **Used by** | `Resource._set_logger()` |
| **Source** | `ocp_resources/resource.py` |

Controls the log level for each resource instance's logger. The logger is created per-resource using the `simple_logger` library.

```bash
export OPENSHIFT_PYTHON_WRAPPER_LOG_LEVEL=DEBUG
```

```python
import os
os.environ["OPENSHIFT_PYTHON_WRAPPER_LOG_LEVEL"] = "DEBUG"

from ocp_resources.pod import Pod
from ocp_resources.resource import get_client

client = get_client()
pod = Pod(name="my-pod", namespace="default", client=client)
# Logger on this Pod instance now uses DEBUG level
```

---

### `OPENSHIFT_PYTHON_WRAPPER_LOG_FILE`

| Property | Value |
|---|---|
| **Type** | `str` |
| **Default** | `""` (empty string — logs to stdout) |
| **Used by** | `Resource._set_logger()` |
| **Source** | `ocp_resources/resource.py` |

Redirects resource log output to a file. When unset or empty, logs are written to stdout.

```bash
export OPENSHIFT_PYTHON_WRAPPER_LOG_FILE=/var/log/ocp-wrapper.log
```

```python
import os
os.environ["OPENSHIFT_PYTHON_WRAPPER_LOG_FILE"] = "/tmp/ocp-wrapper.log"

from ocp_resources.namespace import Namespace
from ocp_resources.resource import get_client

client = get_client()
ns = Namespace(name="test-ns", client=client)
# All log output for this resource is written to /tmp/ocp-wrapper.log
```

---

## Sensitive Data Hashing

### `OPENSHIFT_PYTHON_WRAPPER_HASH_LOG_DATA`

| Property | Value |
|---|---|
| **Type** | `str` |
| **Default** | `"true"` |
| **Valid values** | `"true"`, `"false"` |
| **Used by** | `Resource.hash_resource_dict()` |
| **Source** | `ocp_resources/resource.py` |

Controls whether sensitive fields in resource dictionaries are replaced with `"*******"` when logged. When set to `"false"`, sensitive data is logged in cleartext.

Hashing is applied to fields declared in each resource class's `keys_to_hash` property. The following built-in resources define sensitive keys:

| Resource Class | Hashed Fields |
|---|---|
| `Secret` | `data`, `stringData` |
| `ConfigMap` | `data`, `binaryData` |
| `SealedSecret` | `spec>data`, `spec>encryptedData` |
| `VirtualMachine` | `spec>template>spec>volumes[]>cloudInitNoCloud>userData` |

> **Warning:** Setting this to `"false"` causes sensitive data (e.g., secrets, tokens) to appear in log output. Use only for debugging in secure environments.

```bash
# Disable hashing to see raw values in logs (debug only)
export OPENSHIFT_PYTHON_WRAPPER_HASH_LOG_DATA=false
```

```python
import os
os.environ["OPENSHIFT_PYTHON_WRAPPER_HASH_LOG_DATA"] = "false"

from ocp_resources.secret import Secret
from ocp_resources.resource import get_client

client = get_client()
secret = Secret(name="my-secret", namespace="default", client=client)
# Logs will now show raw secret data instead of "*******"
```

> **Note:** Hashing also depends on the `hash_log_data` constructor parameter on each resource instance. Both must be enabled for hashing to occur. The `hash_log_data` parameter defaults to `True`. See [Resource and NamespacedResource API](resource-api.html) for constructor details.

**Interaction with `hash_log_data` parameter:**

| `OPENSHIFT_PYTHON_WRAPPER_HASH_LOG_DATA` | `hash_log_data` param | Result |
|---|---|---|
| `"true"` (default) | `True` (default) | Sensitive fields hashed |
| `"true"` | `False` | Sensitive fields **not** hashed |
| `"false"` | `True` | Sensitive fields **not** hashed |
| `"false"` | `False` | Sensitive fields **not** hashed |

---

## Resource Reuse (Skip Creation)

### `REUSE_IF_RESOURCE_EXISTS`

| Property | Value |
|---|---|
| **Type** | `str` (YAML dict syntax) |
| **Default** | _(unset — no resources skipped)_ |
| **Used by** | `Resource.deploy()` |
| **Source** | `ocp_resources/resource.py` |

When set, `deploy()` checks whether the target resource already exists on the cluster. If a match is found, the existing resource is returned without creating a new one. This is intended for debugging and iterative development workflows.

> **Warning:** Spaces are significant in the value syntax. The value is parsed as YAML.

**Value format:**

```
{<Kind>: {<name>: <namespace>}}
```

**Matching rules:**

| Pattern | Behavior |
|---|---|
| `{Pod: {}}` | Skip creation for **all** Pods (match by kind only) |
| `{Pod: {my-pod:}}` | Skip creation for Pod named `my-pod` in **any** namespace |
| `{Pod: {my-pod: my-ns}}` | Skip creation for Pod named `my-pod` in namespace `my-ns` only |
| `{Kind1: {}, Kind2: {name: ns}}` | Multiple resource patterns combined |

**Examples:**

```bash
# Skip all Pod creation if the pod already exists
export REUSE_IF_RESOURCE_EXISTS="{Pod: {}}"

# Skip specific pod in specific namespace
export REUSE_IF_RESOURCE_EXISTS="{Pod: {my-pod: my-namespace}}"

# Skip namespace and pod creation
export REUSE_IF_RESOURCE_EXISTS="{Namespace: {test-ns:}, Pod: {my-pod: test-ns}}"
```

```python
from ocp_resources.pod import Pod
from ocp_resources.resource import get_client

client = get_client()

# If REUSE_IF_RESOURCE_EXISTS is set and a matching Pod exists,
# deploy() returns the existing resource without calling create()
pod = Pod(
    name="my-pod",
    namespace="my-namespace",
    client=client,
    containers=[{"name": "test", "image": "nginx"}],
)
pod.deploy()  # Skips creation if resource matches the env var pattern
```

> **Note:** The resource must actually exist on the cluster for the skip to take effect. If the resource does not exist, `deploy()` proceeds with normal creation.

---

## Skip Teardown

### `SKIP_RESOURCE_TEARDOWN`

| Property | Value |
|---|---|
| **Type** | `str` (YAML dict syntax) |
| **Default** | _(unset — no resources skipped)_ |
| **Used by** | `Resource.clean_up()` |
| **Source** | `ocp_resources/resource.py` |

When set, `clean_up()` skips deletion for matching resources and returns `True` without calling `delete()`. This is intended for debugging — preserving resources on the cluster after tests finish.

> **Warning:** Spaces are significant in the value syntax. The value is parsed as YAML.

**Value format:**

Uses the same YAML dict syntax as [`REUSE_IF_RESOURCE_EXISTS`](#reuse_if_resource_exists).

**Matching rules:**

| Pattern | Behavior |
|---|---|
| `{Pod: {}}` | Skip teardown for **all** Pods |
| `{Pod: {my-pod:}}` | Skip teardown for Pod named `my-pod` in **any** namespace |
| `{Pod: {my-pod: my-ns}}` | Skip teardown for Pod named `my-pod` in namespace `my-ns` only |
| `{Kind1: {}, Kind2: {name: ns}}` | Multiple resource patterns combined |

**Examples:**

```bash
# Keep all Namespaces after test run
export SKIP_RESOURCE_TEARDOWN="{Namespace: {}}"

# Keep specific resources
export SKIP_RESOURCE_TEARDOWN="{Namespace: {test-ns:}, Pod: {debug-pod: test-ns}}"
```

```python
from ocp_resources.pod import Pod
from ocp_resources.resource import get_client

client = get_client()
pod = Pod(
    name="debug-pod",
    namespace="test-ns",
    client=client,
    containers=[{"name": "test", "image": "nginx"}],
)
pod.deploy()

# If SKIP_RESOURCE_TEARDOWN is set with a matching pattern,
# clean_up() returns True without deleting the resource
pod.clean_up()  # Resource remains on the cluster
```

> **Tip:** Use `REUSE_IF_RESOURCE_EXISTS` and `SKIP_RESOURCE_TEARDOWN` together for a fast debug loop: skip creation if a resource already exists, and skip teardown so it persists between runs.

```bash
export REUSE_IF_RESOURCE_EXISTS="{Pod: {debug-pod: test-ns}}"
export SKIP_RESOURCE_TEARDOWN="{Pod: {debug-pod: test-ns}}"
```

---

## Combined Usage Patterns

### Debug iteration loop

Set both skip variables to avoid re-creating and tearing down resources between test runs:

```bash
export REUSE_IF_RESOURCE_EXISTS="{Namespace: {test-ns:}, Pod: {my-pod: test-ns}}"
export SKIP_RESOURCE_TEARDOWN="{Namespace: {test-ns:}, Pod: {my-pod: test-ns}}"
```

### Full debug logging with unhashed data

```bash
export OPENSHIFT_PYTHON_WRAPPER_LOG_LEVEL=DEBUG
export OPENSHIFT_PYTHON_WRAPPER_LOG_FILE=/tmp/debug.log
export OPENSHIFT_PYTHON_WRAPPER_HASH_LOG_DATA=false
```

> **Warning:** This combination logs sensitive data in plaintext. Do not use in production or shared environments.

### Custom kubeconfig with proxy

```bash
export KUBECONFIG=/path/to/custom/kubeconfig
export HTTPS_PROXY=http://corporate-proxy.example.com:3128
```

---

## Programmatic Configuration Alternatives

Several behaviors controlled by environment variables can also be set programmatically via constructor parameters or function arguments. See [Resource and NamespacedResource API](resource-api.html) for full constructor parameter documentation.

| Environment Variable | Programmatic Alternative |
|---|---|
| `KUBECONFIG` | `get_client(config_file="/path/to/kubeconfig")` |
| `HTTPS_PROXY` / `HTTP_PROXY` | `client_configuration.proxy = "http://proxy:port"` |
| `OPENSHIFT_PYTHON_WRAPPER_HASH_LOG_DATA` | `Resource(hash_log_data=False, ...)` constructor parameter |

> **Note:** `OPENSHIFT_PYTHON_WRAPPER_LOG_LEVEL`, `OPENSHIFT_PYTHON_WRAPPER_LOG_FILE`, `REUSE_IF_RESOURCE_EXISTS`, and `SKIP_RESOURCE_TEARDOWN` can **only** be configured via environment variables.

## Related Pages

- [Connecting to Clusters](connecting-to-clusters.html)
- [Creating and Managing Resources](creating-and-managing-resources.html)
- [Resource and NamespacedResource API](resource-api.html)
- [Common Resource Patterns](common-patterns.html)
- [Error Handling and Exception Reference](error-handling.html)

---

Source: resource-class-hierarchy.md

Let me start by reading the exclusions file and pages manifest, then explore the codebase thoroughly.

Now let me look at some generated resource subclasses to understand how they extend the base classes:

Now let me look at some more examples - a cluster-scoped resource, the `ResourceConstants`, the class generator, and the `virtual_machine.py` for a more complex subclass:

Now let me look at the `kind` property and how it's derived, plus some other resources for variety:

Now let me look at how the `kind` property works (the `sub_resource_level` function) and the `conversion.py` for the kind name derivation:

Now let me look at how resources discover their API version dynamically:

Let me now look at one more important aspect - how the `get` method works differently in the two base classes, and how `_base_body` differs:

Now let me check the `__init__.py` to see what's exported:

Let me also check for examples:

Now I have enough context to write the documentation. Let me draft it:

# Understanding the Resource Class Hierarchy

Every Kubernetes and OpenShift object you interact with through openshift-python-wrapper — whether it's a Pod, a Namespace, or a VirtualMachine — is a Python class that inherits from a small set of base classes. Understanding how these classes relate to each other helps you know **what capabilities any resource has**, **why some resources require a `namespace` argument and others don't**, and **how new resource types get added to the library**.

This page explains the architecture so you can use existing resources effectively and extend the library when you need to work with a new custom resource.

---

## The Big Picture

The class hierarchy has three conceptual layers:

| Layer | Classes | Purpose |
|---|---|---|
| **Constants** | `ResourceConstants` | Provides shared status strings, condition names, and type constants used across all resources |
| **Base classes** | `Resource`, `NamespacedResource` | Define all CRUD operations, lifecycle management, waiting, validation, and API discovery logic |
| **Concrete resource classes** | `Pod`, `Namespace`, `Deployment`, `VirtualMachine`, and 200+ others | Represent specific Kubernetes/OpenShift resource kinds, adding kind-specific parameters and behavior |

The inheritance flows like this:

```
ResourceConstants
  └── Resource                  ← cluster-scoped resources (Node, Namespace, StorageClass, ClusterRole, …)
        └── NamespacedResource  ← namespace-scoped resources (Pod, Deployment, ConfigMap, Secret, Route, …)
```

Every concrete resource class inherits from either `Resource` (for cluster-scoped resources) or `NamespacedResource` (for namespace-scoped resources). This single design decision controls whether the class requires a `namespace` argument and how it builds API requests.

---

## Key Concepts

### ResourceConstants: Shared Vocabulary

At the root of the hierarchy sits `ResourceConstants`, which defines inner classes for common values:

- **`Status`** — Strings like `RUNNING`, `SUCCEEDED`, `FAILED`, `PENDING`, `ACTIVE`
- **`Condition`** — Condition types like `READY`, `AVAILABLE`, `DEGRADED` and their status values (`TRUE`, `FALSE`, `UNKNOWN`)
- **`Type`** — Service types like `ClusterIP`, `NodePort`, `LoadBalancer`

Because `Resource` inherits from `ResourceConstants`, every resource class in the library can reference these constants:

```python
from ocp_resources.namespace import Namespace

ns.wait_for_status(status=Namespace.Status.ACTIVE, timeout=120)
```

Concrete classes can extend these constants with kind-specific values. For example, `VirtualMachine` adds statuses like `MIGRATING`, `STOPPED`, and `PROVISIONING` to the base `Status` class.

### Resource: The Foundation for Cluster-Scoped Resources

`Resource` is the main base class. It provides everything needed to manage a Kubernetes resource:

| Capability | Methods / Properties |
|---|---|
| **CRUD operations** | `create()`, `delete()`, `update()`, `update_replace()` |
| **Lifecycle management** | `deploy()`, `clean_up()`, context manager (`with` statement) |
| **Querying** | `exists`, `instance`, `status`, `labels` |
| **Waiting** | `wait()`, `wait_deleted()`, `wait_for_status()`, `wait_for_condition()` |
| **Listing** | `get()` class method — yields resource objects matching filters |
| **Validation** | `validate()`, `validate_dict()` |
| **Serialization** | `to_dict()`, `to_yaml()` |
| **API discovery** | Automatic `api_version` resolution from the cluster when only `api_group` is set |

Resources that exist at the cluster level — not inside any namespace — inherit directly from `Resource`. Examples include `Namespace`, `Node`, `StorageClass`, `ClusterRole`, and `CustomResourceDefinition`.

```python
from ocp_resources.namespace import Namespace

# No namespace argument needed — Namespace is cluster-scoped
ns = Namespace(client=client, name="my-namespace")
```

#### Automatic Kind Detection

You never set the `kind` field manually. The `kind` property is a class-level property that automatically derives the Kubernetes kind name from the **class name** using Python's method resolution order (MRO). When you define a class named `StorageClass`, its `kind` is automatically `"StorageClass"`.

#### API Version Discovery

Resources can specify their API identity in two ways:

1. **`api_version`** — A fixed version string (e.g., `"v1"`), used for core Kubernetes resources
2. **`api_group`** — An API group string (e.g., `"apps"`, `"kubevirt.io"`), where the full `apiVersion` is discovered dynamically from the cluster

When only `api_group` is set, the library queries the cluster at runtime to find the latest supported version for that resource kind. This means resource classes automatically work across cluster versions without code changes.

```python
class Namespace(Resource):
    # Core resource — fixed version, no group
    api_version: str = Resource.ApiVersion.V1

class ClusterRole(Resource):
    # Grouped resource — version discovered from cluster
    api_group = Resource.ApiGroup.RBAC_AUTHORIZATION_K8S_IO
```

> **Note:** The `Resource.ApiGroup` and `Resource.ApiVersion` inner classes provide predefined constants for all known API groups and versions. Using these constants avoids typos and makes your code self-documenting.

### NamespacedResource: Adding Namespace Awareness

`NamespacedResource` extends `Resource` with one critical addition: **namespace handling**. It requires a `namespace` argument during construction and injects the namespace into all API calls.

The differences from `Resource` are focused but important:

| Behavior | `Resource` | `NamespacedResource` |
|---|---|---|
| `namespace` required? | No | Yes (unless using `yaml_file` or `kind_dict`) |
| `to_dict()` output | No namespace in metadata | Adds `metadata.namespace` |
| `get()` yields | Objects with `name` only | Objects with both `name` and `namespace` |
| `instance` property | Fetches by name only | Fetches by name and namespace |

```python
from ocp_resources.pod import Pod

# Namespace is required for namespaced resources
pod = Pod(client=client, name="my-pod", namespace="default",
          containers=[{"name": "app", "image": "nginx"}])
```

### Concrete Resource Classes: Where Specifics Live

Concrete classes add three things on top of the base classes:

1. **API identity** — Setting `api_group` or `api_version` to identify which Kubernetes API to call
2. **Constructor parameters** — Typed arguments for the resource's spec fields (like `containers` for Pod, `replicas` for Deployment)
3. **Custom `to_dict()` method** — Builds the Kubernetes resource dictionary from constructor arguments

Here is how a typical generated class is structured:

```python
class Deployment(NamespacedResource):
    # 1. API identity
    api_group: str = NamespacedResource.ApiGroup.APPS

    def __init__(self, replicas=None, selector=None, template=None, **kwargs):
        # 2. Pass common args to base class, store kind-specific args
        super().__init__(**kwargs)
        self.replicas = replicas
        self.selector = selector
        self.template = template

    def to_dict(self):
        # 3. Build the resource dictionary
        super().to_dict()
        if not self.kind_dict and not self.yaml_file:
            self.res["spec"] = {}
            _spec = self.res["spec"]
            _spec["selector"] = self.selector
            _spec["template"] = self.template
            if self.replicas is not None:
                _spec["replicas"] = self.replicas
```

> **Tip:** You can always bypass the typed constructor entirely by passing `yaml_file` or `kind_dict` to any resource class. When you do, the `to_dict()` logic is skipped and the resource is created from your raw definition instead.

#### Adding Kind-Specific Behavior

Many concrete classes go beyond what the generator produces by adding custom methods and properties. For example:

- **Pod** adds `execute()` for running commands, `log()` for reading logs, and a `node` property
- **Deployment** adds `scale_replicas()` and `wait_for_replicas()`
- **Secret** overrides `keys_to_hash` to ensure sensitive data is masked in logs

These additions are preserved across regeneration because the class generator recognizes the `# End of generated code` marker and keeps any code written below it.

### How Generated Classes Are Created

Most concrete resource classes in the library are **code-generated** from the cluster's OpenAPI schema using the `class-generator` tool. The generator:

1. Reads the resource definition from the cluster's OpenAPI schema
2. Determines whether the resource is namespaced (→ `NamespacedResource`) or cluster-scoped (→ `Resource`)
3. Extracts spec fields with their types and descriptions
4. Renders a Python class from a Jinja2 template
5. Preserves any hand-written code below the `# End of generated code` marker

This means the hierarchy is not just an architectural choice — it is **enforced by the code generation pipeline**. Every generated class correctly inherits from the appropriate base class based on the resource's actual scope in Kubernetes.

See [Generating New Resource Classes with class-generator](generating-resource-classes.html) for details on generating classes for new resource types.

---

## How It Affects You

Understanding the hierarchy helps you in several practical ways:

### Knowing What Methods Are Available

Every resource — regardless of kind — inherits the full set of CRUD, waiting, and lifecycle methods from `Resource`. You don't need to check whether a particular resource supports `wait_for_condition()` or context managers; they all do.

```python
# Works for any resource type
with SomeResource(client=client, name="example", **specific_args) as res:
    res.wait_for_condition(condition="Ready", status="True")
```

See [Resource and NamespacedResource API](resource-api.html) for the complete method reference.

### Understanding Constructor Requirements

The base class determines what arguments are mandatory:

| If you're using... | Required arguments |
|---|---|
| `Resource` subclass | `client`, `name` |
| `NamespacedResource` subclass | `client`, `name`, `namespace` |
| Any class with `yaml_file` | `client`, `yaml_file` |
| Any class with `kind_dict` | `client`, `kind_dict` |

See [Creating and Managing Resources](creating-and-managing-resources.html) for full examples of all creation methods.

### Using Status and Condition Constants

The `Status` and `Condition` constants inherited from `ResourceConstants` are available on every resource class. Concrete classes may extend them:

```python
from ocp_resources.virtual_machine import VirtualMachine

# Base constants work on all resources
vm.wait_for_status(status=VirtualMachine.Status.RUNNING)

# Kind-specific constants are added by the concrete class
vm.wait_for_status(status=VirtualMachine.Status.STOPPED)
```

### Extending the Library

If you need to work with a CRD that isn't included in the library, you have two options:

1. **Use the class generator** to scaffold a new class automatically — see [Generating New Resource Classes with class-generator](generating-resource-classes.html)
2. **Write a class manually** by inheriting from `Resource` or `NamespacedResource` and setting `api_group` or `api_version`

A minimal hand-written resource class looks like this:

```python
from ocp_resources.resource import NamespacedResource

class MyCustomResource(NamespacedResource):
    api_group = "example.com"
    # That's it — you get full CRUD, waiting, and lifecycle support
```

> **Warning:** Your class name must match the Kubernetes `kind` exactly (in PascalCase). The `kind` property is derived from the class name automatically — a class named `MyCustomResource` will have `kind = "MyCustomResource"`.

---

## Related Pages

- [Resource and NamespacedResource API](resource-api.html) — Complete method and property reference for the base classes
- [Creating and Managing Resources](creating-and-managing-resources.html) — Practical guide to creating, updating, and deleting resources
- [Generating New Resource Classes with class-generator](generating-resource-classes.html) — Generate Python classes for any CRD
- [Querying and Watching Resources](querying-resources.html) — Use the `get()` class method and watchers to list and observe resources
- [Waiting for Resource Conditions and Status](waiting-for-conditions.html) — Use the waiting methods inherited from the base classes
- [Validating Resources Against OpenAPI Schemas](validating-resources.html) — Validate resources using the built-in schema validation
- [Environment Variables and Configuration](environment-variables.html) — Configure logging, resource reuse, and teardown behavior

## Related Pages

- [Resource and NamespacedResource API](resource-api.html)
- [Generating New Resource Classes with class-generator](generating-resource-classes.html)
- [Creating and Managing Resources](creating-and-managing-resources.html)
- [Schema Validation and Code Generation Architecture](schema-validation-internals.html)
- [Validating Resources Against OpenAPI Schemas](validating-resources.html)

---

Source: schema-validation-internals.md

Let me start by reading the knowledge graph and the pages manifest, then explore the relevant source files.

Now let me look at some more key files - the explain parser, resource base class validation, archive utils, and some test files:

Now let me look at the exceptions module for validation:

Now let me check the CI/CD configuration to understand how schemas are updated:

Let me look at a few generated resource files to understand the output of the code generation:

Let me also check the parsers for status schema generation:

Now let me check what data is actually in the schema file to understand the structure:

Now I have enough information. Let me write the documentation page.

# Schema Validation and Code Generation Architecture

The openshift-python-wrapper uses OpenAPI schemas as the single source of truth for two critical capabilities: **validating resource definitions** before they reach the cluster, and **generating Python wrapper classes** for any Kubernetes or OpenShift resource type. Understanding this architecture helps you diagnose validation errors, generate classes for custom resources, and keep your schemas up to date as clusters evolve.

## The Big Picture

OpenAPI schemas flow through the system in two directions: they are fetched from a live cluster and stored locally, then consumed by the validation engine at runtime and by the code generator at development time.

| Component | Location | Purpose |
|---|---|---|
| **Schema Fetcher** | `class_generator/core/schema.py` | Connects to a cluster, downloads OpenAPI v3 schemas, and writes them to disk |
| **Resource Mappings** | `class_generator/schema/__resources-mappings.json.gz` | Compressed archive mapping each resource kind (lowercase) to its schema(s) |
| **Definitions File** | `class_generator/schema/_definitions.json` | JSON file containing detailed schema definitions with `$ref` targets for nested types |
| **Schema Validator** | `ocp_resources/utils/schema_validator.py` | Runtime engine that loads schemas, resolves `$ref` references, and validates resource dicts |
| **Code Generator** | `class_generator/core/generator.py` | Reads schemas and generates Python class files from a Jinja2 template |
| **Explain Parser** | `class_generator/parsers/explain_parser.py` | Parses the resource mapping to extract fields, types, and group-version-kind metadata |
| **Cluster Version Tracker** | `class_generator/schema/__cluster_version__.txt` | Tracks the cluster version that last produced the schema, controlling update strategy |

### Data Flow: Schema Fetch and Storage

1. **Detect client binary** — The system finds `oc` or falls back to `kubectl` via `get_client_binary()`.
2. **Check cluster version** — `check_and_update_cluster_version()` compares the connected cluster's version against the stored version in `__cluster_version__.txt`.
3. **Determine update strategy** — If the cluster is the same version or newer, all schemas are fetched and existing entries are updated. If older, only missing resources are fetched and existing schemas are preserved.
4. **Fetch OpenAPI v3 index** — `GET /openapi/v3` returns an index of all API group paths (e.g., `api/v1`, `apis/apps/v1`).
5. **Fetch schemas in parallel** — `fetch_all_api_schemas()` downloads schemas from each API path using a thread pool (up to 10 workers).
6. **Build namespacing dictionary** — `build_namespacing_dict()` queries `api-resources` to determine which kinds are namespaced vs. cluster-scoped.
7. **Process definitions** — `process_schema_definitions()` extracts `x-kubernetes-group-version-kind` metadata, builds schema entries, and merges them into the resource mappings. Existing resources are **never deleted** — only added or updated.
8. **Supplement with `oc explain`** — Missing field descriptions, required-field markers, and `$ref` definitions are filled in by running `oc explain` commands in parallel.
9. **Write to disk** — The resource mappings are saved as a gzip-compressed JSON archive (`__resources-mappings.json.gz`), and definitions are written to `_definitions.json`.

### Data Flow: Validation at Runtime

1. **Load on first use** — `SchemaValidator.load_mappings_data()` decompresses and loads the mappings archive and definitions file into class-level caches.
2. **Look up by kind** — `SchemaValidator.load_schema(kind, api_group)` finds the schema for a resource kind (case-insensitive). When multiple API groups define the same kind (e.g., `Ingress` in `networking.k8s.io` and `config.openshift.io`), the `api_group` parameter disambiguates.
3. **Resolve `$ref` references** — The validator recursively resolves all `$ref` pointers against the definitions data, producing a self-contained schema.
4. **Cache resolved schemas** — Resolved schemas are stored in `_schema_cache` keyed by `api_group:kind`, so subsequent validations are fast.
5. **Validate with jsonschema** — `jsonschema.validate()` checks the resource dictionary against the resolved schema.
6. **Format errors** — If validation fails, `format_validation_error()` produces a human-readable message including the field path, error details, and schema context.

### Data Flow: Code Generation

1. **Read resource mappings** — `parse_explain()` loads the mapping file and finds all schema entries for the requested kind.
2. **Select latest API version** — When multiple versions exist within an API group, the latest version is selected using a priority ranking (`v2 > v1 > v1beta2 > v1beta1 > v1alpha2 > v1alpha1`).
3. **Extract fields and types** — The `type_parser` module converts OpenAPI types to Python type annotations and builds parameter dictionaries for the class constructor.
4. **Render Jinja2 template** — `render_jinja_template()` processes `class_generator_template.j2` with the extracted data, producing a complete Python class.
5. **Preserve user code** — If the target file already exists, `parse_user_code_from_file()` extracts any user-added code below the `# End of generated code` marker and re-inserts it after regeneration.
6. **Format and write** — `write_and_format_rendered()` writes the file, then runs `prek` (pre-commit hooks) or falls back to `ruff` for formatting.

## Key Concepts

### The Resource Mappings File

The resource mappings file (`__resources-mappings.json.gz`) is the central schema store. It maps lowercase kind names to arrays of schema objects:

```json
{
  "pod": [
    {
      "description": "Pod is a collection of containers...",
      "properties": { ... },
      "required": [],
      "type": "object",
      "x-kubernetes-group-version-kind": [
        { "group": "", "kind": "Pod", "version": "v1" }
      ],
      "namespaced": true
    }
  ],
  "ingress": [
    { "x-kubernetes-group-version-kind": [{ "group": "networking.k8s.io", ... }], ... },
    { "x-kubernetes-group-version-kind": [{ "group": "config.openshift.io", ... }], ... }
  ]
}
```

Resources with the same kind but different API groups (like `Ingress` above) are stored as separate entries in the array. This structure allows the validator and generator to handle API group disambiguation correctly.

> **Note:** The mappings file is compressed with gzip to reduce repository size. The `archive_utils` module handles transparent compression and decompression via `save_json_archive()` and `load_json_archive()`.

### The Definitions File

The definitions file (`_definitions.json`) contains detailed schema definitions keyed by `group/version/Kind` paths (e.g., `apps/v1/Deployment`, `v1/Pod`). It also stores referenced sub-schemas like `io.k8s.api.core.v1.PodSpec` that are targets of `$ref` pointers. During validation, the `SchemaValidator` uses this file as the reference store for resolving nested types.

### SchemaValidator

`SchemaValidator` is a class with only class-level methods and caches — you never need to instantiate it. It serves as the shared validation engine used by both `Resource.validate()` and `Resource.validate_dict()`.

```python
from ocp_resources.utils.schema_validator import SchemaValidator
```

| Method | Signature | Description |
|---|---|---|
| `load_mappings_data` | `(skip_cache: bool = False) -> bool` | Loads the mappings archive and definitions file. Returns `True` on success. |
| `get_mappings_data` | `(skip_cache: bool = False) -> dict[str, Any] \| None` | Returns loaded mappings data, loading first if needed. |
| `get_definitions_data` | `() -> dict[str, Any] \| None` | Returns loaded definitions data, loading first if needed. |
| `load_schema` | `(kind: str, api_group: str \| None = None) -> dict[str, Any] \| None` | Loads and resolves a complete schema for a kind. Handles API group disambiguation and caching. |
| `validate` | `(resource_dict: dict[str, Any], kind: str, api_group: str \| None = None) -> None` | Validates a resource dict. Raises `jsonschema.ValidationError` on failure. |
| `format_validation_error` | `(error, kind, name, api_group=None) -> str` | Formats a validation error into a user-friendly message with field path and context. |
| `clear_cache` | `() -> None` | Clears the resolved schema cache (not the raw mappings/definitions data). |

> **Tip:** The `load_schema` method resolves `$ref` references recursively. For core Kubernetes types like `ObjectMeta` or `TypeMeta` that may be missing from definitions, it falls back to a permissive `{"type": "object", "additionalProperties": true}` schema rather than failing outright.

### Resource Validation Methods

The `Resource` base class exposes two validation methods that delegate to `SchemaValidator`:

```python
from ocp_resources.resource import Resource

# Instance-level validation — validates self.res
resource.validate()

# Class-level validation — validates any dict against the class's schema
MyResource.validate_dict(resource_dict)
```

Both raise `ocp_resources.exceptions.ValidationError` (not the raw `jsonschema.ValidationError`) with formatted error messages that include the resource identifier, field path, and details.

**Auto-validation** can be enabled per-instance by passing `schema_validation_enabled=True` to the constructor:

```python
from ocp_resources.pod import Pod

pod = Pod(
    name="my-pod",
    namespace="default",
    containers=[{"name": "app", "image": "nginx"}],
    client=client,
    schema_validation_enabled=True,
)
# validation runs automatically before create() and update_replace()
pod.deploy()
```

When auto-validation is enabled:
- `create()` calls `self.validate()` after `to_dict()` builds the resource dictionary
- `update_replace()` calls `validate_dict()` on the replacement resource dictionary
- `update()` (patch operations) does **not** trigger validation because patches are partial documents that may not pass full schema validation

### Schema Update Strategies

The `update_kind_schema()` function employs a version-aware update strategy:

| Cluster Version | Behavior |
|---|---|
| **Same or newer** than last recorded | Fetches **all** API schemas, updates existing resources, supplements with `oc explain` data |
| **Older** than last recorded | Identifies only **missing** resources via `identify_missing_resources()`, fetches only relevant API paths, does **not** modify existing schemas |

This strategy prevents an older cluster from overwriting schemas that contain richer data from a newer cluster.

> **Warning:** If you connect to an older cluster and need to update a specific CRD's schema (for example, after installing a new operator), use `update_single_resource_schema(kind)` or the CLI flag `--update-schema-for <Kind>`. This bypasses the version guard for that one resource.

### Single-Resource Schema Updates

The `update_single_resource_schema()` function fetches the schema for exactly one resource kind without affecting other resources in the mapping:

```python
from class_generator.core.schema import update_single_resource_schema

update_single_resource_schema(kind="LlamaStackDistribution")
```

| Parameter | Type | Description |
|---|---|---|
| `kind` | `str` | The resource Kind (case-sensitive, e.g., `"LlamaStackDistribution"`) |
| `client` | `str \| None` | Client binary path. Auto-detected if `None`. |

**Raises:**
- `ResourceNotFoundError` — if the kind is not found on the cluster
- `RuntimeError` — if schema fetching fails

### Code Generation Pipeline

The `class_generator()` function orchestrates the full generation pipeline:

```python
from class_generator.core.generator import class_generator

generated_files = class_generator(kind="Deployment", overwrite=True)
```

| Parameter | Type | Default | Description |
|---|---|---|---|
| `kind` | `str` | (required) | Kubernetes resource kind |
| `overwrite` | `bool` | `False` | Overwrite existing files |
| `dry_run` | `bool` | `False` | Print output without writing |
| `output_file` | `str` | `""` | Specific output file path |
| `output_dir` | `str` | `""` | Output directory (defaults to `ocp_resources`) |
| `add_tests` | `bool` | `False` | Generate test files in `class_generator/tests/manifests/` |

**Returns:** `list[str]` — paths of generated files.

**Raises:**
- `RuntimeError` — if the kind is not found in schema mappings
- `ValueError` — if the generated filename contains invalid patterns

When a kind exists in multiple API groups (e.g., `Ingress` in both `networking.k8s.io` and `config.openshift.io`), the generator produces **separate files** with a group-based suffix (e.g., `ingress_networking_k8s_io.py`, `ingress_config_openshift_io.py`).

### Generated Class Structure

Every generated file follows a consistent structure produced by the Jinja2 template:

```python
# Generated using https://github.com/RedHatQE/openshift-python-wrapper/blob/main/class_generator/README.md

from typing import Any
from ocp_resources.resource import NamespacedResource

class Pod(NamespacedResource):
    """Pod is a collection of containers that can run on a host."""

    api_version: str = NamespacedResource.ApiVersion.V1

    def __init__(self, containers: list[Any] | None = None, ..., **kwargs: Any) -> None:
        """Args:
            containers (list[Any]): List of containers belonging to the pod.
            ...
        """
        super().__init__(**kwargs)
        self.containers = containers
        ...

    def to_dict(self) -> None:
        super().to_dict()
        if not self.kind_dict and not self.yaml_file:
            # Required fields raise MissingRequiredArgumentError
            # Optional fields added only if not None
            ...
    # End of generated code
```

User-added code placed **after** the `# End of generated code` marker is preserved across regenerations.

### `$ref` Resolution

OpenAPI schemas extensively use `$ref` pointers to reference shared type definitions (e.g., `"$ref": "#/components/schemas/io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta"`). The `SchemaValidator._resolve_refs()` method handles this by:

1. Parsing the reference path to extract the definition name
2. Looking up the definition in `_definitions_data` using multiple key formats (full dotted path, short name, or `version/Kind` format)
3. Recursively resolving any `$ref` pointers within the resolved definition
4. Falling back to a permissive object schema for well-known core types that may be missing

### Schema Supplementation via `oc explain`

The raw OpenAPI v3 schemas from the cluster sometimes lack field descriptions and required-field markers. The system supplements these gaps by:

- Running `oc explain <resource>.<field>` commands **in parallel** for critical resource types (Pod, Deployment, Service, etc.)
- Parsing the explain output to extract field descriptions, types, and required-field markers
- Merging descriptions into existing schema properties where they are missing
- Detecting missing `$ref` targets and fetching their definitions via `oc explain --recursive`

This supplementation only runs during a **full update** (same or newer cluster version), not when only fetching missing resources.

## How It Affects You

| What you do | What happens under the hood |
|---|---|
| Call `resource.validate()` | `SchemaValidator` loads schemas from the compressed archive, resolves `$ref` references, validates with jsonschema, and raises `ValidationError` with a formatted message |
| Pass `schema_validation_enabled=True` | `create()` and `update_replace()` automatically validate before sending to the API server |
| Run `class-generator --update-schema` | Full schema fetch from cluster, version check, parallel API downloads, `oc explain` supplementation, and compressed archive write |
| Run `class-generator -k Pod` | Reads the mapping for `pod`, parses fields/types, renders Jinja2 template, preserves user code, writes and formats the Python file |
| Run `class-generator --update-schema-for MyResource` | Single-resource fetch that bypasses the cluster version guard |
| Run `class-generator --regenerate-all` | Discovers all generated files (by marker comment), regenerates each in parallel, preserving user code |
| Validation reports "No schema found" | The resource kind is not in the mappings archive — run `--update-schema` to fetch it |
| Multiple files generated for one kind | The kind exists in multiple API groups — each group gets its own file with a suffix |

## Related Pages

- See [Validating Resources Against OpenAPI Schemas](validating-resources.html) for a hands-on guide to enabling and using validation.
- See [Generating New Resource Classes with class-generator](generating-resource-classes.html) for step-by-step class generation instructions.
- See [class-generator CLI Reference](class-generator-cli.html) for all CLI options including `--update-schema`, `--update-schema-for`, `--regenerate-all`, and `--coverage-report`.
- See [Understanding the Resource Class Hierarchy](resource-class-hierarchy.html) to learn how generated classes fit into the `Resource` / `NamespacedResource` inheritance tree.
- See [Resource and NamespacedResource API](resource-api.html) for complete documentation of the `validate()` and `validate_dict()` methods.
- See [Testing Without a Cluster Using the Fake Client](testing-without-cluster.html) for how the fake client uses schema-based status templates.

## Related Pages

- [Validating Resources Against OpenAPI Schemas](validating-resources.html)
- [Generating New Resource Classes with class-generator](generating-resource-classes.html)
- [class-generator CLI Reference](class-generator-cli.html)
- [Understanding the Resource Class Hierarchy](resource-class-hierarchy.html)
- [Resource and NamespacedResource API](resource-api.html)

---

Source: working-with-events.md

Let me start by exploring the repository structure and the knowledge graph.

Now let me explore the Event-related source code:

Now let me look at the `_build_event_field_selector` function and related event test files:

Now let me check the `events` test files in the tests directory:

Now let me check the resource.py import and the `events` method more carefully, and also look at how TIMEOUT_4MINUTES is defined:

Let me check where TIMEOUT_4MINUTES is defined in resource.py:

Now let me check the constants value for TIMEOUT_4MINUTES:

Now let me check how the Event class is used in the fake client:

Now let me look at the `_extract_event_info` and the `_process_watch_events` functions to understand the event data structure better:

Now let me look at the event object structure from watch events:

Now let me check how to properly import and use Event:

Let me also look at the test file for events in more detail:

Now let me check the watch method's return structure for events:

Now I have a thorough understanding. Let me write the documentation.

# Working with Kubernetes Events

Monitor, retrieve, and clean up Kubernetes events to debug resource behavior and automate event-driven workflows using the `Event` class and the `resource.events()` method.

## Prerequisites

- A connected Kubernetes/OpenShift client — see [Connecting to Clusters](connecting-to-clusters.html)
- The `ocp_resources` package installed — see [Installing and Creating Your First Resource](quickstart.html)

## Quick Example

Stream events from a namespace in real-time:

```python
from ocp_resources.event import Event

for event in Event.get(client=client, namespace="my-namespace", timeout=30):
    print(f"{event['type']}: {event.object.reason} — {event.object.message}")
```

## Streaming Events in Real-Time with `Event.get()`

`Event.get()` opens a watch connection and yields events as they occur. It's a generator that blocks until the timeout expires or you break out of the loop.

```python
from ocp_resources.event import Event

for event in Event.get(
    client=client,
    namespace="my-namespace",
    timeout=60,
):
    obj = event.object
    print(f"[{obj.type}] {obj.reason}: {obj.message}")
```

Each yielded event is a watch event dictionary with these keys:

| Key            | Description                                                        |
|----------------|--------------------------------------------------------------------|
| `type`         | Watch event type: `ADDED`, `MODIFIED`, or `DELETED`                |
| `object`       | The Event resource object (access `.type`, `.reason`, `.message`, `.involvedObject`, etc.) |
| `raw_object`   | The raw dictionary representation of the event                     |

### `Event.get()` Parameters

| Parameter          | Type                    | Default  | Description                                               |
|--------------------|-------------------------|----------|-----------------------------------------------------------|
| `client`           | `DynamicClient`         | `None`   | Kubernetes dynamic client (required)                      |
| `namespace`        | `str \| None`           | `None`   | Filter events to a specific namespace                     |
| `name`             | `str \| None`           | `None`   | Filter by event name                                      |
| `label_selector`   | `str \| None`           | `None`   | Filter by labels (e.g. `"app=nginx"`)                     |
| `field_selector`   | `str \| None`           | `None`   | Filter by fields (e.g. `"type==Warning"`)                 |
| `resource_version` | `str \| None`           | `None`   | Start watching from a specific resource version           |
| `timeout`          | `int \| None`           | `None`   | Timeout in seconds; `None` watches indefinitely           |

**Returns:** `Generator` — yields watch event dictionaries.

### Filtering with Field Selectors

Field selectors let you narrow down events to exactly what you care about. Combine multiple selectors with commas:

```python
# Only Warning events for ClusterServiceVersion resources
for event in Event.get(
    client=client,
    namespace="my-namespace",
    field_selector="involvedObject.kind==ClusterServiceVersion,type==Warning,reason==AnEventReason",
    timeout=10,
):
    print(event.object.message)
```

Common field selector fields:

| Field                        | Example                                        |
|------------------------------|-------------------------------------------------|
| `type`                       | `type==Warning` or `type==Normal`               |
| `reason`                     | `reason==FailedScheduling`                      |
| `involvedObject.kind`        | `involvedObject.kind==Pod`                      |
| `involvedObject.name`        | `involvedObject.name==my-pod`                   |
| `involvedObject.namespace`   | `involvedObject.namespace==default`             |

## Listing Existing Events with `Event.list()`

Unlike `Event.get()`, which streams events in real-time, `Event.list()` returns existing events immediately as a list. By default it only returns events from the last 5 minutes.

```python
from ocp_resources.event import Event

# List all Warning events from the last 5 minutes
events = Event.list(
    client=client,
    namespace="my-namespace",
    field_selector="type==Warning",
)

for event in events:
    print(f"{event.reason}: {event.message}")
```

Results are sorted by `lastTimestamp` descending (most recent first).

### `Event.list()` Parameters

| Parameter        | Type                | Default | Description                                               |
|------------------|---------------------|---------|-----------------------------------------------------------|
| `client`         | `DynamicClient`     | —       | Kubernetes dynamic client (required)                      |
| `namespace`      | `str \| None`       | `None`  | Filter events to a specific namespace                     |
| `field_selector` | `str \| None`       | `None`  | Filter by fields (e.g. `"type==Warning"`)                 |
| `label_selector` | `str \| None`       | `None`  | Filter by labels                                          |
| `since_seconds`  | `int`               | `300`   | Only return events from the last N seconds                |

**Returns:** `list` — event resource objects sorted by timestamp (most recent first).

**Raises:** `ValueError` — if `since_seconds` is negative.

```python
# Events from the last 30 minutes
events = Event.list(client=client, since_seconds=1800)

# All events (no time filter — uses a very large window)
events = Event.list(client=client, since_seconds=999999)
```

### When to Use `Event.get()` vs `Event.list()`

| Use case                                         | Method         |
|--------------------------------------------------|----------------|
| Watch for new events as they happen               | `Event.get()`  |
| Fetch events that already occurred                | `Event.list()` |
| Collect events during a test run                  | `Event.get()`  |
| Check recent events after a failure               | `Event.list()` |
| Stream events with a timeout                      | `Event.get()`  |
| Get a snapshot sorted by time                     | `Event.list()` |

## Getting Events for a Specific Resource

Every resource instance has an `.events()` method that automatically filters events to that specific resource by setting `involvedObject.name` in the field selector.

```python
from ocp_resources.pod import Pod

pod = Pod(client=client, name="my-pod", namespace="default")

for event in pod.events(timeout=10):
    print(f"{event.object.reason}: {event.object.message}")
```

You can add extra filters on top of the automatic resource filter:

```python
# Only Warning events for this specific pod
for event in pod.events(
    field_selector="type==Warning",
    timeout=10,
):
    print(event.object.message)
```

### `resource.events()` Parameters

| Parameter          | Type    | Default | Description                                               |
|--------------------|---------|---------|-----------------------------------------------------------|
| `name`             | `str`   | `""`    | Filter by event name                                      |
| `label_selector`   | `str`   | `""`    | Filter by labels                                          |
| `field_selector`   | `str`   | `""`    | Additional field selectors (combined with `involvedObject.name` automatically) |
| `resource_version` | `str`   | `""`    | Start watching from a specific resource version           |
| `timeout`          | `int`   | `240`   | Timeout in seconds (default: 4 minutes)                   |

**Returns:** `Generator` — yields watch event dictionaries, same format as `Event.get()`.

> **Note:** The `field_selector` you provide is appended to the automatic `involvedObject.name==<resource-name>` filter. You don't need to specify the resource name yourself.

## Deleting Events with `Event.delete_events()`

Clean up events before a test run to avoid false positives from stale events:

```python
from ocp_resources.event import Event

# Delete all events in a namespace
Event.delete_events(client=client, namespace="my-namespace")

# Delete events matching a specific reason
Event.delete_events(
    client=client,
    namespace="my-namespace",
    field_selector="reason==AnEventReason",
)
```

### `Event.delete_events()` Parameters

| Parameter          | Type                    | Default  | Description                                               |
|--------------------|-------------------------|----------|-----------------------------------------------------------|
| `client`           | `DynamicClient`         | `None`   | Kubernetes dynamic client (required)                      |
| `namespace`        | `str \| None`           | `None`   | Target namespace                                          |
| `name`             | `str \| None`           | `None`   | Specific event name to delete                             |
| `label_selector`   | `str \| None`           | `None`   | Filter by labels                                          |
| `field_selector`   | `str \| None`           | `None`   | Filter by fields                                          |
| `resource_version` | `str \| None`           | `None`   | Filter by resource version                                |
| `timeout`          | `int \| None`           | `None`   | Timeout in seconds                                        |

**Returns:** `None`

## Advanced Usage

### Test Setup: Clean Events Before Each Test

A common pattern is deleting events before a test so that only events generated during the test are captured:

```python
from ocp_resources.event import Event


def test_pod_scheduling(client, namespace):
    # Clean slate — remove old events
    Event.delete_events(client=client, namespace=namespace)

    # ... create resources and trigger the behavior under test ...

    # Verify expected events occurred
    events = Event.list(
        client=client,
        namespace=namespace,
        field_selector="reason==Scheduled",
        since_seconds=60,
    )
    assert len(events) > 0, "Pod was not scheduled"
```

### Collecting Events During an Operation

Use `Event.get()` with a timeout to capture all events that occur during an operation:

```python
from ocp_resources.event import Event

events = []
for event in Event.get(
    client=client,
    namespace="my-namespace",
    field_selector="involvedObject.kind==Deployment",
    timeout=30,
):
    events.append(event.object)
    if event.object.reason == "ScalingReplicaSet":
        break  # Found what we were looking for

print(f"Captured {len(events)} events")
```

### Watching Cluster-Wide Events

Omit the `namespace` parameter to watch events across all namespaces:

```python
from ocp_resources.event import Event

# Watch all Warning events cluster-wide
for event in Event.get(
    client=client,
    field_selector="type==Warning",
    timeout=60,
):
    ns = event.object.involvedObject.get("namespace", "cluster-scoped")
    print(f"[{ns}] {event.object.reason}: {event.object.message}")
```

### Accessing Event Object Properties

The `event.object` yielded by `Event.get()` and `resource.events()` is a Kubernetes `ResourceInstance` with these commonly used attributes:

| Attribute                     | Description                                  |
|-------------------------------|----------------------------------------------|
| `event.object.type`           | `"Normal"` or `"Warning"`                    |
| `event.object.reason`         | Short reason string (e.g. `"Scheduled"`)     |
| `event.object.message`        | Human-readable event message                 |
| `event.object.count`          | Number of times this event occurred          |
| `event.object.firstTimestamp`  | When the event first occurred                |
| `event.object.lastTimestamp`   | When the event most recently occurred        |
| `event.object.source`         | Dict with `component` and `host`             |
| `event.object.involvedObject` | Dict with `kind`, `name`, `namespace`, etc.  |

## Troubleshooting

**`Event.get()` hangs indefinitely**
You likely omitted the `timeout` parameter. Always set a `timeout` value to avoid blocking forever:

```python
# Bad — hangs if no events arrive
for event in Event.get(client=client, namespace="ns"):
    ...

# Good — stops after 30 seconds
for event in Event.get(client=client, namespace="ns", timeout=30):
    ...
```

**`Event.list()` returns an empty list**
- Check the `since_seconds` value. The default is 300 (5 minutes). If the events are older, increase the value.
- Verify the `namespace` is correct.
- Ensure the `field_selector` syntax uses `==` (double equals), not `=`.

**`resource.events()` returns events for other resources too**
This method filters only by `involvedObject.name`. If multiple resources share the same name across different kinds, add a `field_selector` to narrow it down:

```python
for event in pod.events(
    field_selector="involvedObject.kind==Pod",
    timeout=10,
):
    print(event.object.message)
```

> **Warning:** The `dyn_client` parameter on `Event.get()` and `Event.delete_events()` is deprecated and will be removed in the next major release. Use `client` instead.

## Related Pages

- [Querying and Watching Resources](querying-resources.html)
- [Common Resource Patterns](common-patterns.html)
- [Executing Commands in Pods and Retrieving Logs](pod-execution-and-logs.html)
- [Waiting for Resource Conditions and Status](waiting-for-conditions.html)
- [Creating and Managing Resources](creating-and-managing-resources.html)

---

Source: pod-execution-and-logs.md

Let me explore the repository structure and understand the codebase first.

Now let me check the `read_namespaced_pod_log` kwargs available, and look at how the MCP server uses log parameters:

Now let me check the import paths and verify the `get_client` function:

Now let me check test files for pod execution tests:

Now let me also verify whether the Node class has a `name` property:

Now I have all the information I need to write the documentation. Let me compose it:

# Executing Commands in Pods and Retrieving Logs

Run commands inside running pods and retrieve their logs to debug and interact with workloads on your OpenShift or Kubernetes cluster.

## Prerequisites

- A connected cluster client (see [Connecting to Clusters](connecting-to-clusters.html))
- An existing, running pod you want to interact with

## Quick Example

```python
from ocp_resources.pod import Pod
from ocp_resources.resource import get_client

client = get_client()
pod = Pod(client=client, name="my-app", namespace="default")

# Run a command inside the pod
output = pod.execute(command=["echo", "hello"])
print(output)  # "hello\n"

# Get pod logs
logs = pod.log()
print(logs)
```

## Executing Commands with `Pod.execute()`

`Pod.execute()` runs a command inside a pod container using the Kubernetes exec API and returns the standard output as a string.

### Method Signature

```python
Pod.execute(
    command: list[str],
    timeout: int = 60,
    container: str = "",
    ignore_rc: bool = False,
) -> str
```

| Parameter   | Type         | Default | Description                                                                                 |
|-------------|--------------|---------|---------------------------------------------------------------------------------------------|
| `command`   | `list[str]`  | —       | The command to run, as a list of strings (e.g., `["ls", "-la", "/tmp"]`)                    |
| `timeout`   | `int`        | `60`    | Maximum seconds to wait for the command to complete                                         |
| `container` | `str`        | `""`    | Container name to execute in. If empty, uses the first container in the pod spec            |
| `ignore_rc` | `bool`       | `False` | When `True`, return stdout even if the command exits with a non-zero return code            |

**Returns:** `str` — the standard output of the command.

**Raises:** `ExecOnPodError` — when the command fails (non-zero exit code) and `ignore_rc` is `False`.

### Step-by-Step: Running a Simple Command

1. Get a reference to the pod:

```python
from ocp_resources.pod import Pod
from ocp_resources.resource import get_client

client = get_client()
pod = Pod(client=client, name="my-app", namespace="my-namespace")
```

2. Execute a command:

```python
output = pod.execute(command=["cat", "/etc/hostname"])
print(output)
```

3. Use the result for further logic:

```python
files = pod.execute(command=["ls", "/app/data"])
for filename in files.strip().split("\n"):
    print(f"Found: {filename}")
```

### Selecting a Container

For multi-container pods, specify which container to run the command in. If you omit `container`, the first container defined in the pod spec is used.

```python
# Execute in a specific container
output = pod.execute(
    command=["cat", "/var/log/app.log"],
    container="sidecar",
)
```

### Setting a Timeout

Long-running commands may need a longer timeout. The default is 60 seconds.

```python
# Allow up to 5 minutes for a heavy operation
output = pod.execute(
    command=["pg_dump", "mydb"],
    timeout=300,
)
```

When the timeout is exceeded, an `ExecOnPodError` is raised with the error message `"stream resp is closed"`.

### Ignoring Non-Zero Exit Codes

Some commands return a non-zero exit code as part of normal operation (e.g., `grep` returns 1 when no match is found). Use `ignore_rc=True` to get the output regardless:

```python
# grep returns exit code 1 when there's no match — don't treat that as an error
output = pod.execute(
    command=["grep", "ERROR", "/var/log/app.log"],
    ignore_rc=True,
)
if output:
    print("Errors found:", output)
else:
    print("No errors in logs")
```

### Handling Errors with `ExecOnPodError`

When a command fails, `ExecOnPodError` provides structured access to the failure details.

```python
from ocp_resources.pod import Pod
from ocp_resources.exceptions import ExecOnPodError

try:
    pod.execute(command=["ls", "/nonexistent"])
except ExecOnPodError as e:
    print(f"Command: {e.cmd}")      # ['ls', '/nonexistent']
    print(f"Return code: {e.rc}")   # Non-zero exit code (or -1 for stream errors)
    print(f"Stdout: {e.out}")       # Standard output captured before failure
    print(f"Stderr: {e.err}")       # Standard error or error channel details
```

`ExecOnPodError` attributes:

| Attribute | Type        | Description                                                          |
|-----------|-------------|----------------------------------------------------------------------|
| `cmd`     | `list[str]` | The command that was executed                                        |
| `rc`      | `int`       | Return code (`-1` for stream/timeout errors, otherwise the exit code)|
| `out`     | `str`       | Standard output captured from the command                            |
| `err`     | `str` or `dict` | Standard error output, or the Kubernetes error channel response  |

> **Tip:** For a complete list of all custom exceptions, see [Error Handling and Exception Reference](error-handling.html).

## Retrieving Logs with `Pod.log()`

`Pod.log()` returns the logs from a pod container as a string. It passes keyword arguments directly to the Kubernetes `read_namespaced_pod_log` API.

### Basic Usage

```python
logs = pod.log()
print(logs)
```

### Common Keyword Arguments

Pass any parameter supported by the Kubernetes `read_namespaced_pod_log` API:

| Parameter       | Type   | Description                                               |
|-----------------|--------|-----------------------------------------------------------|
| `container`     | `str`  | Container name to get logs from (required for multi-container pods) |
| `previous`      | `bool` | Return logs from a previous terminated container instance  |
| `tail_lines`    | `int`  | Number of lines from the end of the logs to return         |
| `since_seconds` | `int`  | Only return logs newer than this many seconds              |

### Examples

```python
# Get logs from a specific container
logs = pod.log(container="nginx")

# Get the last 50 lines
logs = pod.log(tail_lines=50)

# Get logs from the last 5 minutes
logs = pod.log(since_seconds=300)

# Get logs from a crashed container's previous instance
logs = pod.log(container="app", previous=True)
```

## Accessing Pod Properties

Beyond executing commands and reading logs, the `Pod` class provides properties for inspecting the pod's runtime state.

### Getting the Node

The `node` property returns a `Node` object for the node where the pod is scheduled:

```python
from ocp_resources.pod import Pod
from ocp_resources.resource import get_client

client = get_client()

for pod in Pod.get(client=client, label_selector="app=my-app"):
    node = pod.node
    print(f"Pod {pod.name} is running on node {node.name}")
```

> **Note:** The `node` property raises an `AssertionError` if the pod has not yet been scheduled to a node.

### Getting the Pod IP Address

The `ip` property returns the pod's IP address from its status:

```python
pod_ip = pod.ip
print(f"Pod IP: {pod_ip}")
```

### Getting the Pod Status

The `status` property (inherited from the base resource class) returns the pod's phase:

```python
print(f"Pod status: {pod.status}")  # e.g., "Running", "Pending", "Succeeded"
```

## Advanced Usage

### Iterating Over Pods with Selectors

Combine `Pod.get()` with `execute()` or `log()` to debug across multiple pods:

```python
from ocp_resources.pod import Pod
from ocp_resources.exceptions import ExecOnPodError
from ocp_resources.resource import get_client

client = get_client()

for pod in Pod.get(client=client, namespace="production", label_selector="app=web"):
    try:
        uptime = pod.execute(command=["uptime"])
        print(f"{pod.name} on {pod.node.name}: {uptime.strip()}")
    except ExecOnPodError as e:
        print(f"{pod.name}: command failed — {e}")
```

### Executing in Multi-Container Pods

When pods contain sidecar containers (e.g., logging or proxy containers), always specify the target container explicitly:

```python
# Main application container
app_output = pod.execute(command=["cat", "/app/config.yaml"], container="app")

# Envoy sidecar
proxy_stats = pod.execute(command=["curl", "localhost:15000/stats"], container="istio-proxy")

# Logs from each container
app_logs = pod.log(container="app", tail_lines=100)
proxy_logs = pod.log(container="istio-proxy", tail_lines=100)
```

### Collecting Debug Information

A practical recipe for gathering debugging data from a running pod:

```python
from ocp_resources.pod import Pod
from ocp_resources.exceptions import ExecOnPodError
from ocp_resources.resource import get_client

client = get_client()
pod = Pod(client=client, name="my-app", namespace="default")

debug_info = {
    "pod_name": pod.name,
    "node": pod.node.name,
    "ip": pod.ip,
    "status": pod.status,
    "logs_tail": pod.log(tail_lines=20),
}

# Safely attempt commands that might fail
for cmd_name, cmd in [("env", ["env"]), ("df", ["df", "-h"]), ("ps", ["ps", "aux"])]:
    try:
        debug_info[cmd_name] = pod.execute(command=cmd, timeout=10)
    except ExecOnPodError:
        debug_info[cmd_name] = "command failed"

for key, value in debug_info.items():
    print(f"--- {key} ---\n{value}\n")
```

### Using Pod Context Manager for Temporary Pods

Create a pod, run commands, and clean up automatically:

```python
from ocp_resources.pod import Pod
from ocp_resources.resource import get_client

client = get_client()

with Pod(
    client=client,
    name="debug-pod",
    namespace="default",
    containers=[{
        "name": "debug",
        "image": "registry.access.redhat.com/ubi9/ubi:latest",
        "command": ["sleep", "3600"],
    }],
) as pod:
    pod.wait_for_status(status=Pod.Status.RUNNING, timeout=120)
    output = pod.execute(command=["cat", "/etc/os-release"])
    print(output)
# Pod is automatically deleted when the context manager exits
```

> **Note:** See [Creating and Managing Resources](creating-and-managing-resources.html) for more about context managers and resource lifecycle.

## Troubleshooting

### `ExecOnPodError` with `"stream resp is closed"`

This typically means the command timed out. Increase the `timeout` parameter:

```python
pod.execute(command=["long-running-script.sh"], timeout=300)
```

### `AssertionError: Node not found for pod <name>`

The pod is not yet scheduled. Wait for it to be running before accessing `pod.node`:

```python
pod.wait_for_status(status=Pod.Status.RUNNING, timeout=120)
node = pod.node
```

### `ExecOnPodError` with return code `-1` and an error channel dict

This indicates a Kubernetes-level failure (e.g., container not found, pod not running). Check that:

- The pod is in `Running` status
- The `container` name matches one defined in the pod spec
- The container has started and is not in `CrashLoopBackOff`

### Logs return empty string

The container may not have produced any output yet, or it may have restarted. Try:

```python
# Check previous container instance
logs = pod.log(previous=True)

# Or narrow to recent logs
logs = pod.log(since_seconds=60)
```

## Related Pages

- [Common Resource Patterns](common-patterns.html)
- [Working with Kubernetes Events](working-with-events.html)
- [Error Handling and Exception Reference](error-handling.html)
- [Creating and Managing Resources](creating-and-managing-resources.html)
- [Working with Virtual Machines (KubeVirt)](working-with-virtual-machines.html)

---

Source: error-handling.md

Let me explore the repository structure and find all relevant exception definitions and usages.

Now let me look at the `ConditionError` usage more closely and the `MissingTemplateVariables` and `MissingResourceError` exceptions:

Now let me look at how the deploy method uses ValidationError:

Now let me see where `validate` is called during `create`:

Now let me look at the `Pod.execute` method more closely:

Now let me look at how `client_with_basic_auth` works:

Now let me see the NNCP methods in full context:

Let me also look at `MissingTemplateVariables` usage more closely:

Now let me check the `__init__` of Resource to see how `schema_validation_enabled` is handled:

Now let me also check the `wait_for_condition` method signature more closely:

Let me also look at `MissingResourceResError` usage:

Now let me check the test for `ResourceTeardownError`:

Let me see how SecretTestExit is defined:

Let me check the import path for all exceptions:

Now I have a thorough understanding of the codebase. Let me write the reference documentation.

# Error Handling and Exception Reference

All custom exceptions are defined in `ocp_resources.exceptions` and can be imported directly:

```python
from ocp_resources.exceptions import (
    ExecOnPodError,
    MissingRequiredArgumentError,
    ResourceTeardownError,
    ValidationError,
    ConditionError,
    NNCPConfigurationFailed,
    ClientWithBasicAuthError,
    MissingResourceError,
    MissingTemplateVariables,
)
```

> **Tip:** All exceptions inherit from Python's built-in `Exception` class and can be caught with a bare `except Exception` if needed.

---

## ExecOnPodError

Raised when a command executed inside a pod via `Pod.execute()` fails. See [Executing Commands in Pods and Retrieving Logs](pod-execution-and-logs.html) for full usage details.

**Import:**

```python
from ocp_resources.exceptions import ExecOnPodError
```

**Constructor Parameters:**

| Parameter | Type | Description |
|-----------|------|-------------|
| `command` | `list[str]` | The command that was executed |
| `rc` | `int` | Return code (`-1` if the return code could not be determined) |
| `out` | `str` | Standard output captured from the command |
| `err` | `Any` | Standard error output or error channel details |

**Attributes on the caught exception:**

| Attribute | Type | Description |
|-----------|------|-------------|
| `cmd` | `list[str]` | The command that was executed |
| `rc` | `int` | The return code |
| `out` | `str` | Standard output |
| `err` | `Any` | Standard error or error channel dict |

**Raised by:** `Pod.execute()` in the following scenarios:

- Command times out (stream response closes before completion) — `rc=-1`
- Error channel returns no status — `rc=-1`
- Error channel status is `"Failure"` — `rc=-1`, `err` contains the full error channel dict
- Command exits with a non-zero exit code — `rc` contains the actual exit code

**String representation:**

```
Command execution failure: ['ls', '/nonexistent'], RC: 2, OUT: , ERR: ls: cannot access '/nonexistent'
```

**Example:**

```python
from ocp_resources.pod import Pod
from ocp_resources.exceptions import ExecOnPodError

pod = Pod(client=client, name="my-pod", namespace="default")

try:
    output = pod.execute(command=["ls", "/nonexistent"], timeout=30)
except ExecOnPodError as e:
    print(f"Command: {e.cmd}")
    print(f"Return code: {e.rc}")
    print(f"Stdout: {e.out}")
    print(f"Stderr: {e.err}")
```

**Handling non-zero exit codes without exceptions:**

```python
# Use ignore_rc=True to suppress ExecOnPodError on non-zero exit codes
output = pod.execute(command=["grep", "pattern", "/var/log/messages"], ignore_rc=True)
```

---

## MissingRequiredArgumentError

Raised when a resource is instantiated without providing required arguments and no `yaml_file` or `kind_dict` was supplied. This error is raised during `to_dict()`, which is called automatically by `create()` and `deploy()`.

**Import:**

```python
from ocp_resources.exceptions import MissingRequiredArgumentError
```

**Constructor Parameters:**

| Parameter | Type | Description |
|-----------|------|-------------|
| `argument` | `str` | Name of the missing required argument(s) |

**Attributes on the caught exception:**

| Attribute | Type | Description |
|-----------|------|-------------|
| `argument` | `str` | The missing argument name |

**String representation:**

```
Missing required argument/s. Either provide yaml_file, kind_dict or pass self.containers
```

**Raised by:** The `to_dict()` method of resource subclasses when required spec fields are not provided. Examples of resources that raise this exception:

| Resource Class | Required Arguments |
|---|---|
| `Pod` | `containers` |
| `StorageClass` | `provisioner` |
| `ClusterRoleBinding` | `cluster_role` |
| `ClusterResourceQuota` | `quota`, `selector` |
| `CronJob` | `job_template`, `schedule` |
| `InferenceService` | `predictor` |
| `IPAddressPool` | `addresses` |
| `ResourceQuota` | `hard` |
| `UserDefinedNetwork` | `topology` |

> **Note:** This exception is **not** raised if you construct the resource using `yaml_file` or `kind_dict`, since those bypass the `to_dict()` logic.

**Example:**

```python
from ocp_resources.pod import Pod
from ocp_resources.exceptions import MissingRequiredArgumentError

try:
    pod = Pod(client=client, name="my-pod", namespace="default")
    pod.deploy()
except MissingRequiredArgumentError as e:
    print(f"Missing: {e.argument}")
    # Output: Missing required argument/s. Either provide yaml_file, kind_dict or pass self.containers
```

---

## ResourceTeardownError

Raised when a resource's `clean_up()` method returns `False` during context manager exit. This indicates the resource could not be deleted.

**Import:**

```python
from ocp_resources.exceptions import ResourceTeardownError
```

**Constructor Parameters:**

| Parameter | Type | Description |
|-----------|------|-------------|
| `resource` | `Any` | The resource object that failed to tear down |

**Attributes on the caught exception:**

| Attribute | Type | Description |
|-----------|------|-------------|
| `resource` | `Any` | The resource object that failed teardown |

**String representation:**

```
Failed to execute teardown for resource <resource>
```

**Raised by:** `Resource.__exit__()` — the context manager exit handler. Specifically, this is raised when:

1. The resource was created with `teardown=True` (the default).
2. The `clean_up()` method returns `False`.

**Example:**

```python
from ocp_resources.pod import Pod
from ocp_resources.exceptions import ResourceTeardownError

try:
    with Pod(
        client=client,
        name="my-pod",
        namespace="default",
        containers=[{"name": "nginx", "image": "nginx:latest"}],
    ) as pod:
        # Use pod...
        pass
    # clean_up() is called automatically here
except ResourceTeardownError as e:
    print(f"Could not delete: {e.resource}")
```

> **Tip:** Set `teardown=False` on the resource constructor if you do not want automatic cleanup on context manager exit, and thus never want this exception raised.

---

## ValidationError

Raised when a resource fails schema validation against the OpenAPI specification. See [Validating Resources Against OpenAPI Schemas](validating-resources.html) for full validation guide.

**Import:**

```python
from ocp_resources.exceptions import ValidationError
```

**Constructor Parameters:**

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `message` | `str` | *(required)* | Human-readable error description |
| `path` | `str` | `""` | JSONPath to the invalid field (e.g., `"spec.containers[0].image"`) |
| `schema_error` | `Any` | `None` | Original `jsonschema` validation error for debugging |

**Attributes on the caught exception:**

| Attribute | Type | Description |
|-----------|------|-------------|
| `message` | `str` | Human-readable error message |
| `path` | `str` | JSONPath to the invalid field |
| `schema_error` | `Any` | Original `jsonschema.ValidationError` if available |

**String representation:**

```
Validation error at 'spec.containers[0].image': Invalid type
```

When `path` is empty:

```
Validation error: Field is required
```

**Raised by:**

| Method | Trigger |
|--------|---------|
| `resource.validate()` | Called explicitly by user |
| `resource.create()` | When `schema_validation_enabled=True` |
| `resource.update_replace()` | When `schema_validation_enabled=True` |
| `Resource.validate_dict()` | Class method for validating raw dicts |

> **Note:** `resource.update()` does **not** trigger validation even when `schema_validation_enabled=True`, because updates send partial patches that would fail full schema validation.

**Example — explicit validation:**

```python
from ocp_resources.pod import Pod
from ocp_resources.exceptions import ValidationError

pod = Pod(client=client, name="my-pod", namespace="default",
          containers=[{"name": "nginx", "image": "nginx:latest"}])

try:
    pod.validate()
except ValidationError as e:
    print(f"Error: {e.message}")
    print(f"Path: {e.path}")
    if e.schema_error:
        print(f"Original error: {e.schema_error}")
```

**Example — auto-validation on create:**

```python
from ocp_resources.pod import Pod
from ocp_resources.exceptions import ValidationError

try:
    pod = Pod(
        client=client,
        name="my-pod",
        namespace="default",
        containers=[{"name": "nginx", "image": "nginx:latest"}],
        schema_validation_enabled=True,
    )
    pod.deploy()
except ValidationError as e:
    print(f"Invalid resource: {e}")
```

**Example — validate a raw dictionary:**

```python
from ocp_resources.deployment import Deployment
from ocp_resources.exceptions import ValidationError

deployment_dict = {
    "apiVersion": "apps/v1",
    "kind": "Deployment",
    "metadata": {"name": "my-deploy"},
    "spec": {"replicas": "three"},  # Wrong type — should be int
}

try:
    Deployment.validate_dict(resource_dict=deployment_dict)
except ValidationError as e:
    print(f"Validation failed: {e}")
```

---

## ConditionError

Raised when a resource reaches an undesired stop condition during `wait_for_condition()`. See [Waiting for Resource Conditions and Status](waiting-for-conditions.html) for details.

**Import:**

```python
from ocp_resources.exceptions import ConditionError
```

**Constructor Parameters:** Standard `Exception` — accepts a single string message.

**Raised by:** `Resource.wait_for_condition()` when the `stop_condition` parameter matches before the desired condition is met.

**`wait_for_condition()` parameters relevant to `ConditionError`:**

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `condition` | `str` | *(required)* | Condition type to wait for |
| `status` | `str` | *(required)* | Expected status value |
| `timeout` | `int` | `300` | Maximum wait time in seconds |
| `sleep_time` | `int` | `1` | Polling interval in seconds |
| `stop_condition` | `str \| None` | `None` | Condition type that should abort the wait |
| `stop_status` | `str` | `"True"` | Status value for the stop condition |

**String representation:**

```
Deployment my-deploy reached stop_condition 'Failed' in status 'True':
{'type': 'Failed', 'status': 'True', 'reason': 'DeadlineExceeded', 'message': '...'}
```

> **Note:** When `stop_condition` is `None` (the default), `ConditionError` is never raised. Instead, `TimeoutExpiredError` is raised if the desired condition is not met within the timeout.

**Example:**

```python
from ocp_resources.deployment import Deployment
from ocp_resources.exceptions import ConditionError
from timeout_sampler import TimeoutExpiredError

deploy = Deployment(client=client, name="my-deploy", namespace="default")

try:
    deploy.wait_for_condition(
        condition="Available",
        status="True",
        timeout=120,
        stop_condition="Failed",
        stop_status="True",
    )
except ConditionError as e:
    print(f"Resource entered failure state: {e}")
except TimeoutExpiredError:
    print("Timed out waiting for condition")
```

---

## NNCPConfigurationFailed

Raised when a `NodeNetworkConfigurationPolicy` (NNCP) fails to configure on one or more nodes.

**Import:**

```python
from ocp_resources.exceptions import NNCPConfigurationFailed
```

**Constructor Parameters:** Standard `Exception` — accepts a single string message describing the failure reason and error details.

**Raised by:** `NodeNetworkConfigurationPolicy.wait_for_status_success()` in two scenarios:

| Scenario | Message Pattern |
|----------|-----------------|
| No matching node found | `"{name}. Reason: NoMatchingNode"` |
| Configuration failed on nodes | `"Reason: FailedToConfigure\n{error_details}"` |

> **Note:** When `wait_for_status_success()` catches `TimeoutExpiredError` or `NNCPConfigurationFailed`, it logs the error with node details and re-raises the exception.

**Example:**

```python
from ocp_resources.node_network_configuration_policy import NodeNetworkConfigurationPolicy
from ocp_resources.exceptions import NNCPConfigurationFailed
from timeout_sampler import TimeoutExpiredError

nncp = NodeNetworkConfigurationPolicy(
    client=client,
    name="my-nncp",
    desired_state={"interfaces": [{"name": "eth1", "type": "ethernet", "state": "up"}]},
)

try:
    nncp.deploy()
    nncp.wait_for_status_success()
except NNCPConfigurationFailed as e:
    print(f"NNCP configuration failed: {e}")
except TimeoutExpiredError:
    print("NNCP configuration timed out")
```

---

## ClientWithBasicAuthError

Raised during OAuth-based client authentication when using username/password credentials to connect to an OpenShift cluster. See [Connecting to Clusters](connecting-to-clusters.html) for connection methods.

**Import:**

```python
from ocp_resources.exceptions import ClientWithBasicAuthError
```

**Constructor Parameters:** Standard `Exception` — accepts a single string message.

**Raised by:** `client_configuration_with_basic_auth()` in the following scenarios:

| Scenario | Message |
|----------|---------|
| OAuth well-known endpoint not reachable | `"No well-known file found at endpoint"` |
| Authorization code not returned after login | `"No authorization code found"` |
| No `authorization_endpoint` in OAuth config | `"No authorization_endpoint found in well-known file"` |
| Token exchange fails | `"Failed to authenticate with basic auth"` |

**Example:**

```python
from ocp_resources.exceptions import ClientWithBasicAuthError
from ocp_resources.resource import client_configuration_with_basic_auth

import kubernetes

configuration = kubernetes.client.Configuration()
configuration.verify_ssl = False

try:
    api_client = client_configuration_with_basic_auth(
        username="admin",
        password="password",
        host="https://api.cluster.example.com:6443",
        configuration=configuration,
    )
except ClientWithBasicAuthError as e:
    print(f"Authentication failed: {e}")
```

---

## MissingResourceError

Raised when a resource object fails to generate its internal representation.

**Import:**

```python
from ocp_resources.exceptions import MissingResourceError
```

**Constructor Parameters:**

| Parameter | Type | Description |
|-----------|------|-------------|
| `name` | `str` | Name of the resource that could not be generated |

**Attributes on the caught exception:**

| Attribute | Type | Description |
|-----------|------|-------------|
| `resource_name` | `str` | The resource name |

**String representation:**

```
Failed to generate resource: my-resource
```

**Example:**

```python
from ocp_resources.exceptions import MissingResourceError

try:
    # Operations that may fail to generate a resource
    ...
except MissingResourceError as e:
    print(f"Resource generation failed: {e.resource_name}")
```

---

## MissingTemplateVariables

Raised when rendering a YAML template and not all required template variables have been provided.

**Import:**

```python
from ocp_resources.exceptions import MissingTemplateVariables
```

**Constructor Parameters:**

| Parameter | Type | Description |
|-----------|------|-------------|
| `var` | `str` | Name of the missing template variable |
| `template` | `str` | Path to the template file |

**Attributes on the caught exception:**

| Attribute | Type | Description |
|-----------|------|-------------|
| `var` | `str` | The missing variable name |
| `template` | `str` | The template file path |

**String representation:**

```
Missing variables image for template tests/manifests/vm.yaml
```

**Example:**

```python
from ocp_resources.exceptions import MissingTemplateVariables

try:
    result = generate_yaml_from_template(name="my-vm")
    # If template also requires 'image', raises MissingTemplateVariables
except MissingTemplateVariables as e:
    print(f"Variable '{e.var}' missing in template '{e.template}'")
```

---

## Common Error-Handling Patterns

### Catching Multiple Library Exceptions

```python
from ocp_resources.exceptions import (
    ExecOnPodError,
    MissingRequiredArgumentError,
    ResourceTeardownError,
    ValidationError,
)

try:
    with Pod(client=client, name="worker", namespace="default",
             containers=[{"name": "app", "image": "myapp:latest"}],
             schema_validation_enabled=True) as pod:
        pod.execute(command=["python", "run_task.py"])
except ValidationError as e:
    print(f"Invalid resource spec: {e}")
except ExecOnPodError as e:
    print(f"Task failed (rc={e.rc}): {e.err}")
except ResourceTeardownError as e:
    print(f"Pod cleanup failed: {e}")
except MissingRequiredArgumentError as e:
    print(f"Missing field: {e.argument}")
```

### Safe Condition Waiting with Early Abort

```python
from ocp_resources.exceptions import ConditionError
from timeout_sampler import TimeoutExpiredError

try:
    resource.wait_for_condition(
        condition="Ready",
        status="True",
        timeout=180,
        stop_condition="Failed",
        stop_status="True",
    )
except ConditionError:
    # Resource entered a terminal failure state — no point waiting further
    resource.clean_up()
    raise
except TimeoutExpiredError:
    # Condition not met within timeout — may be transient
    print("Resource is taking too long, investigating...")
```

### Ignoring Non-Critical Command Failures

```python
from ocp_resources.exceptions import ExecOnPodError

# Method 1: Use ignore_rc=True
output = pod.execute(command=["grep", "ERROR", "/var/log/app.log"], ignore_rc=True)

# Method 2: Catch and inspect the return code
try:
    output = pod.execute(command=["test", "-f", "/tmp/lockfile"])
except ExecOnPodError as e:
    if e.rc == 1:
        print("Lock file does not exist — proceeding")
    else:
        raise  # Re-raise unexpected errors
```

### Validation Before Batch Deployment

```python
from ocp_resources.exceptions import ValidationError

resources = [pod1, pod2, deployment1, service1]

# Pre-validate all resources before deploying any
errors = []
for resource in resources:
    try:
        resource.validate()
    except ValidationError as e:
        errors.append((resource.name, str(e)))

if errors:
    for name, err in errors:
        print(f"  {name}: {err}")
    raise SystemExit("Fix validation errors before deploying")

for resource in resources:
    resource.deploy()
```

---

## Exception Hierarchy Summary

| Exception | Parent | Module |
|-----------|--------|--------|
| `ExecOnPodError` | `Exception` | `ocp_resources.exceptions` |
| `MissingRequiredArgumentError` | `Exception` | `ocp_resources.exceptions` |
| `ResourceTeardownError` | `Exception` | `ocp_resources.exceptions` |
| `ValidationError` | `Exception` | `ocp_resources.exceptions` |
| `ConditionError` | `Exception` | `ocp_resources.exceptions` |
| `NNCPConfigurationFailed` | `Exception` | `ocp_resources.exceptions` |
| `ClientWithBasicAuthError` | `Exception` | `ocp_resources.exceptions` |
| `MissingResourceError` | `Exception` | `ocp_resources.exceptions` |
| `MissingTemplateVariables` | `Exception` | `ocp_resources.exceptions` |

> **Warning:** `MissingResourceResError` is deprecated and will be removed in a future release. Use `MissingResourceError` instead.

## Related Pages

- [Creating and Managing Resources](creating-and-managing-resources.html)
- [Waiting for Resource Conditions and Status](waiting-for-conditions.html)
- [Validating Resources Against OpenAPI Schemas](validating-resources.html)
- [Executing Commands in Pods and Retrieving Logs](pod-execution-and-logs.html)
- [Connecting to Clusters](connecting-to-clusters.html)

---
