Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,16 @@ 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 required on `Resource` / `NamespacedResource` (and on `.get()` / `Event` APIs). Create it with `get_client()` (or `get_client(fake=True)` in tests); do not rely on implicit kubeconfig/client creation.
- 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 examples/validation_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
from ocp_resources.resource import get_client
from ocp_resources.service import Service

FAKE_CLIENT = get_client(fake=True)


def print_section(title):
"""Print a section header"""
Expand All @@ -30,6 +32,7 @@ def demo_basic_validation():
# Create a valid pod
print("1. Creating a valid pod and validating...")
pod = Pod(
client=FAKE_CLIENT,
name="nginx-pod",
namespace="default",
containers=[{"name": "nginx", "image": "nginx:latest", "ports": [{"containerPort": 80}]}],
Expand All @@ -44,6 +47,7 @@ def demo_basic_validation():
# Create an invalid pod (missing required fields)
print("\n2. Creating an invalid pod (missing image)...")
invalid_pod = Pod(
client=FAKE_CLIENT,
name="invalid-pod",
namespace="default",
containers=[{"name": "nginx"}], # Missing required 'image' field
Expand All @@ -61,7 +65,7 @@ def demo_auto_validation():
print_section(title="Auto-validation During Create")

# Get a fake client for demo
client = get_client(fake=True)
client = FAKE_CLIENT

# Create pod with auto-validation enabled
print("1. Creating pod with auto-validation enabled...")
Expand Down Expand Up @@ -141,6 +145,7 @@ def demo_different_resources():
# Service validation
print("1. Validating a Service...")
service = Service(
client=FAKE_CLIENT,
name="nginx-service",
namespace="default",
selector={"app": "nginx"},
Expand All @@ -156,6 +161,7 @@ def demo_different_resources():
# ConfigMap validation
print("\n2. Validating a ConfigMap...")
config_map = ConfigMap(
client=FAKE_CLIENT,
name="app-config",
namespace="default",
data={"app.properties": "debug=true\nport=8080", "database.url": "postgresql://localhost:5432/mydb"},
Expand All @@ -176,7 +182,12 @@ def demo_performance():
pods = []
for i in range(5):
pods.append(
Pod(name=f"perf-test-pod-{i}", namespace="default", containers=[{"name": "nginx", "image": "nginx:latest"}])
Pod(
client=FAKE_CLIENT,
name=f"perf-test-pod-{i}",
namespace="default",
containers=[{"name": "nginx", "image": "nginx:latest"}],
)
)

# First validation (loads and caches schema)
Expand Down Expand Up @@ -208,6 +219,7 @@ def demo_error_details():
# Create pod with multiple errors
print("Creating a pod with multiple validation errors...")
pod = Pod(
client=FAKE_CLIENT,
name="invalid-name-", # Names must start and end with alphanumeric, can only contain lowercase alphanumeric or hyphens
namespace="default",
containers=[
Expand Down
12 changes: 11 additions & 1 deletion examples/validation_troubleshooting.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@
from ocp_resources.deployment import Deployment
from ocp_resources.exceptions import ValidationError
from ocp_resources.pod import Pod
from ocp_resources.resource import get_client
from ocp_resources.service import Service

FAKE_CLIENT = get_client(fake=True)


def print_error_case(title, description):
"""Print an error case header"""
Expand All @@ -35,14 +38,15 @@ def case_1_missing_required_fields():
print("Problem code:")
print("""
pod = Pod(
client=client,
name="my-pod",
namespace="default",
containers=[{"name": "nginx"}] # Missing 'image'
)
""")

try:
pod = Pod(name="my-pod", namespace="default", containers=[{"name": "nginx"}])
pod = Pod(client=FAKE_CLIENT, name="my-pod", namespace="default", containers=[{"name": "nginx"}])
pod.validate()
except ValidationError as e:
print(f"Error: {e}")
Expand All @@ -52,6 +56,7 @@ def case_1_missing_required_fields():
print("Fixed code:")
print("""
pod = Pod(
client=client,
name="my-pod",
namespace="default",
containers=[{
Expand Down Expand Up @@ -119,13 +124,15 @@ def case_3_invalid_field_values():
print("Problem code:")
print("""
pod = Pod(
client=client,
name="My-Pod-123", # Capital letters not allowed
namespace="default"
)
""")

try:
pod = Pod(
client=FAKE_CLIENT,
name="My-Pod-123", # Invalid DNS name
namespace="default",
containers=[{"name": "nginx", "image": "nginx:latest"}],
Expand All @@ -139,6 +146,7 @@ def case_3_invalid_field_values():
print("Fixed code:")
print("""
pod = Pod(
client=client,
name="my-pod-123", # Valid DNS name
namespace="default"
)
Expand All @@ -155,6 +163,7 @@ def case_4_invalid_structure():
print("Problem code:")
print("""
service = Service(
client=client,
name="my-service",
namespace="default",
selector={"app": "nginx"},
Expand Down Expand Up @@ -182,6 +191,7 @@ def case_4_invalid_structure():
print("Fixed code:")
print("""
service = Service(
client=client,
name="my-service",
namespace="default",
selector={"app": "nginx"},
Expand Down
40 changes: 11 additions & 29 deletions ocp_resources/event.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import warnings
from collections.abc import Generator
from datetime import datetime, timedelta, timezone
from typing import Any
Expand All @@ -16,29 +15,10 @@ class Event:

api_version = "v1"

# TODO: remove once `client` is mandatory
@staticmethod
def _resolve_client(
client: DynamicClient | None,
dyn_client: DynamicClient | None,
) -> DynamicClient:
"""Resolve client from new or deprecated parameter with deprecation warning."""
if client is None and dyn_client is not None:
warnings.warn(
"`dyn_client` arg will be renamed to `client` and will be mandatory in the next major release.",
FutureWarning,
stacklevel=3, # Adjusted for helper function call
)

resolved = client or dyn_client
assert resolved is not None, "Either 'client' or 'dyn_client' must be provided"
return resolved

@classmethod
def get(
cls,
client: DynamicClient | None = None, # TODO: make mandatory in the next major release
dyn_client: DynamicClient | None = None, # TODO: remove in the next major release
client: DynamicClient,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
namespace: str | None = None,
name: str | None = None,
label_selector: str | None = None,
Expand All @@ -51,7 +31,6 @@ def get(

Args:
client (DynamicClient): K8s client
dyn_client (DynamicClient): K8s client
namespace (str): event namespace
name (str): event name
label_selector (str): filter events by labels; comma separated string of key=value
Expand All @@ -72,7 +51,8 @@ def get(
):
print(event.object)
"""
_client = cls._resolve_client(client, dyn_client)
if client is None:
raise TypeError("client is required")

LOGGER.info("Reading events")
LOGGER.debug(
Expand All @@ -81,7 +61,7 @@ def get(
f" resource_version={resource_version}, timeout={timeout}"
)

event_listener = _client.resources.get(api_version=cls.api_version, kind=cls.__name__)
event_listener = client.resources.get(api_version=cls.api_version, kind=cls.__name__)
yield from event_listener.watch(
namespace=namespace,
name=name,
Expand Down Expand Up @@ -137,6 +117,9 @@ def list(
field_selector="type==Warning",
)
"""
if client is None:
raise TypeError("client is required")

if since_seconds < 0:
raise ValueError("since_seconds must be >= 0")

Expand Down Expand Up @@ -172,8 +155,7 @@ def list(
@classmethod
def delete_events(
cls,
client: DynamicClient | None = None, # TODO: make mandatory in the next major release
dyn_client: DynamicClient | None = None, # TODO: remove in the next major release
client: DynamicClient,
namespace: str | None = None,
name: str | None = None,
label_selector: str | None = None,
Expand All @@ -187,7 +169,6 @@ def delete_events(

Args:
client (DynamicClient): K8s client
dyn_client (DynamicClient): K8s client
namespace (str): event namespace
name (str): event name
label_selector (str): filter events by labels; comma separated string of key=value
Expand All @@ -200,7 +181,8 @@ def delete_events(
def delete_events_before_test(client):
Event.delete_events(client=client, namespace="my-namespace", field_selector="reason=AnEventReason")
"""
_client = cls._resolve_client(client, dyn_client)
if client is None:
raise TypeError("client is required")

LOGGER.info("Deleting events")
LOGGER.debug(
Expand All @@ -209,7 +191,7 @@ def delete_events_before_test(client):
f" resource_version={resource_version}, timeout={timeout}"
)

_client.resources.get(api_version=cls.api_version, kind=cls.__name__).delete(
client.resources.get(api_version=cls.api_version, kind=cls.__name__).delete(
namespace=namespace,
name=name,
label_selector=label_selector,
Expand Down
Loading