Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
80cd7ea
feat(resource)!: require DynamicClient client
rnetser Aug 26, 2026
2429e3b
fix(resource): address Qodo type-hint and docstring findings
rnetser Aug 26, 2026
635c5f3
test(resource): tighten client=None assertions and cover Event.list
rnetser Sep 1, 2026
4189863
add client to NodeNetworkState
rnetser Oct 8, 2026
ae6a182
feat(resource)!: make client keyword-only and required
rnetser Nov 4, 2026
4014960
fix(resource): address review findings on client API
rnetser Nov 4, 2026
36c0aa7
fix(resource): polish client API docs and error message
rnetser Nov 4, 2026
9f5f93a
fix(network): type OVN overlay MTU as int
rnetser Nov 4, 2026
3702f3f
fix(examples): pass an explicit client in role and NNCP examples
rnetser Nov 4, 2026
5c5aa2d
fix(examples): give the auto-validation demo its own fake client
rnetser Nov 4, 2026
c5b61ac
test(examples): pin the rule that examples pass an explicit client
rnetser Nov 5, 2026
7a21ee6
test(examples): harden the client guard and fix special_cases
rnetser Nov 5, 2026
437893e
test(examples): tighten the client guard against false results
rnetser Nov 5, 2026
b6d5696
fix(tests): cover all example client call forms
rnetser Nov 5, 2026
1288f79
Merge branch 'main' of github.com:RedHatQE/openshift-python-wrapper i…
rnetser Nov 8, 2026
702a83b
docs(resource): document the mandatory client contract
rnetser Nov 8, 2026
e7095c0
fix(docs): make the two Qodo-flagged snippets runnable
rnetser Nov 8, 2026
b273688
Merge branch 'main' of github.com:RedHatQE/openshift-python-wrapper i…
rnetser Nov 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,17 @@ Code between markers is auto-generated — do NOT modify manually. Use `class-ge

## When Writing Code

- **Always pass `client` when instantiating a resource** — `client: DynamicClient` is a required keyword argument on `Resource` / `NamespacedResource` and required on their `.get()` / `get_all_cluster_resources()` methods, plus the `Event` APIs. Create it with `get_client()` (or `get_client(fake=True)` in tests); do not rely on implicit kubeconfig/client creation.
- **`dyn_client`, `config_file`, `config_dict` and `context` are no longer accepted** on resource APIs — they raise `TypeError`. Use `client=` instead; `config_file`, `config_dict` and `context` still belong to `get_client()`.
- No client-side validation in `to_dict()` — let the K8s/OCP API server return errors. Helper functions in resource classes CAN validate.
- Context managers for auto-cleanup: `with Pod(name="test", namespace="default") as pod:`
- Context managers for auto-cleanup: `with Pod(client=client, name="test", namespace="default") as pod:`
- Wait utilities: `wait_for_status()`, `wait_for_condition()` via timeout-sampler
- Sensitive data: add keys to `keys_to_hash` property for automatic log hashing
- Fake client for tests: `get_client(fake=True)` — no cluster required

## When Reviewing Code

- Verify resources are constructed with an explicit `client=` (and `.get()` / `Event` calls pass `client`)
- Verify generated code markers are intact — no manual edits between markers
- Check type hints on all new functions/parameters
- Verify tests exist for new helper methods (not needed for generated `__init__`/`to_dict`)
Expand Down
16 changes: 14 additions & 2 deletions docs/connecting-to-clusters.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ 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`.
> **Note:** `client` is a **required keyword-only** argument on every `Resource` / `NamespacedResource` constructor and a required first argument on `get()`, `get_all_cluster_resources()` and the `Event` APIs. Nothing is created implicitly: omitting it raises `TypeError` and `client=None` raises `MissingClientError`. The removed `dyn_client`, `config_file`, `config_dict` and `context` keywords now raise `TypeError` - they belong to `get_client()` only. See [Migrating to the Mandatory `client`](resource-api.html#migrating-to-the-mandatory-client).

## Environment Variables

Expand Down Expand Up @@ -289,7 +289,19 @@ When multiple authentication parameters are provided, `get_client()` uses this p

## Troubleshooting

**"FutureWarning: 'client' arg will be mandatory"**
**"TypeError: Unsupported argument(s): config_file"**
You passed a client-config keyword to a resource instead of to `get_client()`. `config_file`, `config_dict`, `context` and `dyn_client` are no longer resource arguments:

```python
# Wrong - the kubeconfig kwargs belong to get_client() and raise TypeError:
# Namespace(config_file="~/.kube/config", name="my-ns")

# Right
client = get_client(config_file="~/.kube/config")
ns = Namespace(client=client, name="my-ns")
```

**"TypeError: ... missing 1 required keyword-only argument: 'client'"**
You are creating a resource without passing a `client`. Always call `get_client()` first and pass the result:

```python
Expand Down
2 changes: 1 addition & 1 deletion docs/creating-and-managing-resources.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ All resource classes accept these common parameters:

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `client` | `DynamicClient` | `None` | Cluster client connection (will be required in next major release) |
| `client` | `DynamicClient` | **required** | Cluster client connection; keyword-only. Obtain it from `get_client()` |
| `name` | `str` | `None` | Resource name |
| `namespace` | `str` | `None` | Namespace (namespaced resources only) |
| `teardown` | `bool` | `True` | Whether `clean_up()` deletes the resource |
Expand Down
10 changes: 5 additions & 5 deletions docs/editing-resources-temporarily.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Temporarily modify Kubernetes and OpenShift resources during tests and have thei
from ocp_resources.resource import ResourceEditor
from ocp_resources.node import Node

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

with ResourceEditor(patches={node: {"metadata": {"labels": {"test-label": "true"}}}}):
# The node now has the label "test-label": "true"
Expand All @@ -36,7 +36,7 @@ That's it—when the `with` block ends (even if an exception occurs), the origin
from ocp_resources.resource import ResourceEditor
from ocp_resources.config_map import ConfigMap

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

### 2. Define your patch
Expand Down Expand Up @@ -69,8 +69,8 @@ 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")
ns = Namespace(client=client, name="test-ns")
cm = ConfigMap(client=client, name="app-config", namespace="test-ns")

patches = {
ns: {"metadata": {"labels": {"env": "test"}}},
Expand All @@ -91,7 +91,7 @@ If you need finer control over when patches are applied and reverted—for examp
from ocp_resources.resource import ResourceEditor
from ocp_resources.config_map import ConfigMap

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

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

Expand Down
49 changes: 49 additions & 0 deletions docs/error-handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ All custom exceptions are defined in `ocp_resources.exceptions` and can be impor
```python
from ocp_resources.exceptions import (
ExecOnPodError,
MissingClientError,
MissingRequiredArgumentError,
ResourceTeardownError,
ValidationError,
Expand Down Expand Up @@ -147,6 +148,53 @@ except MissingRequiredArgumentError as e:

---

## MissingClientError

Raised when a `Resource` / `NamespacedResource` constructor, `get()`, `get_all_cluster_resources()`, or an `Event` API (`Event.get()`, `Event.list()`, `Event.delete_events()`) receives `client=None`. The library no longer creates a client implicitly, so `client` must come from `get_client()`.

`MissingClientError` subclasses the built-in `TypeError`, so existing `except TypeError` handlers keep working.

**Import:**

```python
from ocp_resources.exceptions import MissingClientError
```

**Constructor Parameters:** None - the message is generated by `__str__()`, and a custom message is used as-is if one is passed.

**String representation:**

```
client is required. Pass a DynamicClient from get_client() or get_client(fake=True).
```

**Raised by:**

| Entry point | Condition |
|---|---|
| `Resource(...)` / `NamespacedResource(...)` | `client=None` (omitting `client` entirely raises the plain `TypeError` for the missing keyword-only argument) |
| `Resource.get()` / `NamespacedResource.get()` | `client=None` |
| `Resource.get_all_cluster_resources()` | `client=None` - validated eagerly, before the returned generator is iterated |
| `Event.get()` / `Event.list()` / `Event.delete_events()` | `client=None` |

**Example:**

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

try:
pod = Pod(client=None, name="my-pod", namespace="default")
except MissingClientError as e:
print(e) # client is required. Pass a DynamicClient from get_client() or get_client(fake=True).
```

> **Note:** Omitting `client` altogether never reaches `MissingClientError` - Python rejects the missing keyword-only argument first, with `TypeError: ... missing 1 required keyword-only argument: 'client'`.

> **Note:** Passing the removed `dyn_client`, `config_file`, `config_dict` or `context` keywords raises a plain `TypeError` with an `Unsupported argument(s): ...` message instead - see [Migrating to the Mandatory `client`](resource-api.html#migrating-to-the-mandatory-client).

---

## ResourceTeardownError

Raised when a resource's `clean_up()` method returns `False` during context manager exit. This indicates the resource could not be deleted.
Expand Down Expand Up @@ -648,6 +696,7 @@ for resource in resources:
| Exception | Parent | Module |
|-----------|--------|--------|
| `ExecOnPodError` | `Exception` | `ocp_resources.exceptions` |
| `MissingClientError` | `TypeError` | `ocp_resources.exceptions` |
| `MissingRequiredArgumentError` | `Exception` | `ocp_resources.exceptions` |
| `ResourceTeardownError` | `Exception` | `ocp_resources.exceptions` |
| `ValidationError` | `Exception` | `ocp_resources.exceptions` |
Expand Down
1 change: 1 addition & 0 deletions docs/generating-resource-classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ This creates a Python file in `ocp_resources/` with a fully typed class you can
from ocp_resources.deployment import Deployment

dep = Deployment(
client=client,
name="my-app",
namespace="default",
replicas=3,
Expand Down
6 changes: 3 additions & 3 deletions docs/querying-resources.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,18 +109,18 @@ for resource in Resource.get_all_cluster_resources(client=client, label_selector
@classmethod
def get(
cls,
client: DynamicClient | None = None,
client: DynamicClient,
*,
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) |
| `client` | `DynamicClient` | Kubernetes dynamic client (required; positional or keyword) |
| `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 |
Expand Down
1 change: 1 addition & 0 deletions docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ You can also validate manually without creating:

```python
pod = Pod(
client=client,
name="check-me",
namespace="default",
containers=[{"name": "nginx", "image": "nginx:latest"}],
Expand Down
76 changes: 53 additions & 23 deletions docs/resource-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,38 @@ See [Connecting to Clusters](connecting-to-clusters.html) for connection pattern

---

## Migrating to the Mandatory `client`

`client` is now a **required keyword-only** argument on `Resource` / `NamespacedResource`, and a required first argument on the `.get()`, `.get_all_cluster_resources()` and `Event` APIs. The library no longer creates a client for you, so nothing happens implicitly from `KUBECONFIG` or `~/.kube/config`.

| Before | After |
|---|---|
| `Pod(name="p", namespace="ns")` | `Pod(client=client, name="p", namespace="ns")` |
| `Pod(dyn_client=client, ...)` | `Pod(client=client, ...)` - `dyn_client` raises `TypeError` |
| `Pod(config_file="~/.kube/config", ...)` | `Pod(client=get_client(config_file="~/.kube/config"), ...)` |
| `Pod(config_dict={...}, ...)` | `Pod(client=get_client(config_dict={...}), ...)` |
| `Pod(context="prod", ...)` | `Pod(client=get_client(context="prod"), ...)` |
| `Pod.get(dyn_client=client, ...)` | `Pod.get(client=client, ...)` |
| `Event.get(dyn_client=client, ...)` | `Event.get(client=client, ...)` |

`config_file`, `config_dict` and `context` still exist - they now belong to `get_client()` only.

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

client = get_client() # once, at session/module setup
pod = Pod(client=client, name="my-pod", namespace="default")
```

### Failure modes to know about

- Passing a removed keyword (`dyn_client`, `config_file`, `config_dict`, `context`) raises `TypeError: Unsupported argument(s): ...` with the migration hint.
- Omitting `client` raises `TypeError` (`... missing 1 required keyword-only argument: 'client'`); passing `client=None` raises `MissingClientError`, whose message names `get_client()`.
- Removing `client` from the middle of a legacy constructor signature means old **positional** calls no longer fail loudly - the argument silently binds to the next parameter. For example `Secret("n", "ns", client)` used to set `client` and now sets `accesskeyid`; `BridgeNetworkAttachmentDefinition(...)` used to take `client` as the 7th positional and now takes `mtu` there. Pass `client` by keyword and construct these resources with keywords only.

---

## `Resource`

```python
Expand All @@ -91,25 +123,23 @@ See [Understanding the Resource Class Hierarchy](resource-class-hierarchy.html)
### Constructor

```python
Resource(
client=client,
name="my-resource",
)
Resource(name="my-resource", client=client)
```

The real signature places `client` behind a bare `*`, so it must be passed as a keyword:
`def __init__(self, name=None, *, client, teardown=True, ...)`. `name` stays positional-or-keyword;
anything after `*` (including `client`) is keyword-only.

| Parameter | Type | Default | Description |
|---|---|---|---|
| `client` | `DynamicClient \| None` | `None` | Kubernetes dynamic client. Will be mandatory in the next major release. |
| `client` | `DynamicClient` | **required** | Kubernetes dynamic client; keyword-only. Obtain it from `get_client()` (or `get_client(fake=True)` in tests). A missing or `None` client raises (`MissingClientError` subclasses `TypeError`). |
| `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`. |
Expand All @@ -119,11 +149,15 @@ Resource(
| `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()`. |

`config_file`, `config_dict` and `context` are **not** constructor arguments any more - they belong to `get_client()`. Passing them (or `dyn_client`) through a resource constructor raises `TypeError` with a migration hint. See [Migrating to the Mandatory `client`](#migrating-to-the-mandatory-client).

**Raises:**

| Exception | Condition |
|---|---|
| `ValueError` | Both `yaml_file` and `kind_dict` are provided. |
| `TypeError` | `dyn_client`, `config_file`, `config_dict`, `context`, or any unknown keyword argument is passed. |
| `MissingClientError` | `client` is `None` (a `TypeError` subclass, so existing `except TypeError` handlers keep working). |
| `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. |
Expand Down Expand Up @@ -424,14 +458,11 @@ Get the resource API object with optional filtering kwargs.
@classmethod
def get(
cls,
client: DynamicClient | None = None,
dyn_client: DynamicClient | None = None,
config_file: str = "",
client: DynamicClient,
*,
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]
```
Expand All @@ -440,17 +471,18 @@ 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`. |
| `client` | `DynamicClient` | **required** | Kubernetes client; positional or keyword. Obtain it from `get_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.

**Raises:** `MissingClientError` if `client` is `None`; `TypeError` if `dyn_client`, `config_file`, `config_dict` or `context` is passed, or if an extra positional argument is given (`singular_name` and the rest are keyword-only).

`dyn_client`, `config_file` and `context` are no longer accepted - see [Migrating to the Mandatory `client`](#migrating-to-the-mandatory-client).

> **Note:** For `Resource` (cluster-scoped), yielded objects are constructed with `name` only. For `NamespacedResource`, yielded objects include both `name` and `namespace`.

```python
Expand All @@ -477,11 +509,7 @@ See [Querying and Watching Resources](querying-resources.html) for advanced list
```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,
client: DynamicClient,
**kwargs: Any,
) -> Generator[ResourceField, None, None]
```
Expand All @@ -490,11 +518,13 @@ Yield all resources across **all** API groups in the cluster.

| Parameter | Type | Default | Description |
|---|---|---|---|
| `client` | `DynamicClient \| None` | `None` | Kubernetes client. |
| `client` | `DynamicClient` | **required** | Kubernetes client; positional or keyword. Obtain it from `get_client()`. |
| `**kwargs` | | | Passed to the API (e.g., `label_selector`). |

**Yields:** `ResourceField`

**Raises:** `MissingClientError` if `client` is `None`; `TypeError` if `dyn_client`, `config_file`, `config_dict` or `context` is passed, or if an extra positional argument is given. `client` is validated before iteration starts, so a bad call fails immediately instead of on the first `next()`.

```python
for resource in Resource.get_all_cluster_resources(client=client, label_selector="app=myapp"):
print(f"{resource.kind}: {resource.metadata.name}")
Expand Down
2 changes: 2 additions & 0 deletions docs/validating-resources.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Catch configuration mistakes before they reach the Kubernetes API by validating
from ocp_resources.pod import Pod

pod = Pod(
client=client,
name="nginx-pod",
namespace="default",
containers=[{"name": "nginx", "image": "nginx:latest"}],
Expand All @@ -33,6 +34,7 @@ from ocp_resources.exceptions import ValidationError
from ocp_resources.service import Service

service = Service(
client=client,
name="nginx-service",
namespace="default",
selector={"app": "nginx"},
Expand Down
2 changes: 1 addition & 1 deletion docs/working-with-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ for event in pod.events(
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.
> **Note:** `client` is a required argument on all `Event` APIs (`Event.get()`, `Event.list()`, `Event.delete_events()`). The old `dyn_client` parameter has been removed and now raises `TypeError` - pass `client=get_client()` instead. A `client=None` call raises `MissingClientError`, and `Event.get()` validates the client before returning its generator, so a bad call fails immediately.

## Related Pages

Expand Down
Loading
Loading