mirror of
https://github.com/imjasonh/client-go2
synced 2026-07-08 00:55:56 +00:00
This PR adds support for resource-specific expansion methods and implements
missing standard client-go operations to achieve feature parity.
## New Features
### Expansion Methods
- Added PodClient with methods: GetLogs, Bind, Evict, ProxyGet
- Added ServiceClient with method: ProxyGet
- Both clients implement the official k8s.io/client-go expansion interfaces
- Runtime type assertions ensure type safety (panic if wrong type)
### Missing Standard Methods
- Watch: Watch resources with optional label/field selectors
- DeleteCollection: Delete multiple resources matching criteria
- UpdateStatus: Update only the status subresource
### Other Improvements
- Refactored all tests to use inline configuration pattern
- Added comprehensive e2e tests for new functionality
- Updated documentation with examples
- Fixed linting error in stream.Close() handling
## Design Decisions
The expansion methods follow a namespace-scoped pattern where calling
`client.PodClient("namespace")` returns a client that implements the
standard client-go PodExpansion interface. This maintains full API
compatibility while providing type safety through runtime assertions.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
139 lines
4.1 KiB
Markdown
139 lines
4.1 KiB
Markdown
## Experimenting with `k8s.io/client-go` and Go generics
|
|
|
|
[](https://github.com/imjasonh/client-go2/actions/workflows/build.yaml)
|
|
|
|
This is an experimental type-parameter-aware client that wraps [`k8s.io/client-go/rest`](https://pkg.go.dev/k8s.io/client-go/rest), in [about 570 lines of mostly-vibe-coded Go](./generic/client.go).
|
|
|
|
## Features
|
|
|
|
- **Type-safe generic client** - Work with strongly-typed Kubernetes objects instead of `unstructured.Unstructured`
|
|
- **Zero code generation** - Uses Go generics instead of code generation
|
|
- **Full CRUD operations** - List, Get, Create, Update, Delete, Patch, Watch, DeleteCollection, and UpdateStatus support
|
|
- **Informer support** - Watch for changes with type-safe event handlers
|
|
- **Automatic GVR inference** - No need to manually specify GroupVersionResource for standard Kubernetes types
|
|
- **Expansion methods** - Resource-specific operations like Pod.GetLogs() and Service.ProxyGet()
|
|
- **Label/Field selectors** - Filter resources using Kubernetes selectors
|
|
- **SubResource access** - Generic method to access any subresource
|
|
|
|
## Usage
|
|
|
|
See [the example](./main.go) for comprehensive usage examples.
|
|
|
|
### Quick Examples
|
|
|
|
#### Basic CRUD Operations
|
|
```go
|
|
// Create a client with automatic GVR inference
|
|
client, err := generic.NewClient[*corev1.Pod](config)
|
|
|
|
// List pods with label selector
|
|
pods, err := client.List(ctx, "default", &metav1.ListOptions{
|
|
LabelSelector: "app=nginx",
|
|
})
|
|
|
|
// Get a specific pod
|
|
pod, err := client.Get(ctx, "default", "my-pod", nil)
|
|
```
|
|
|
|
#### Expansion Methods for Pods (client-go compatible)
|
|
```go
|
|
// Start with a generic client for pods
|
|
client, err := generic.NewClient[*corev1.Pod](config)
|
|
|
|
// Get a namespace-scoped PodClient that implements typedcorev1.PodExpansion
|
|
podClient := client.PodClient("default") // Will panic if T is not *corev1.Pod
|
|
|
|
// Get pod logs (matches client-go API)
|
|
req := podClient.GetLogs("my-pod", &corev1.PodLogOptions{
|
|
Container: "nginx",
|
|
TailLines: &tailLines,
|
|
})
|
|
logs, err := req.DoRaw(ctx)
|
|
|
|
// Bind pod to node
|
|
err = podClient.Bind(ctx, binding, metav1.CreateOptions{})
|
|
|
|
// Evict pod
|
|
err = podClient.Evict(ctx, eviction)
|
|
|
|
// For cluster-scoped operations, use the generic client directly
|
|
pods, err := client.List(ctx, "default", nil)
|
|
```
|
|
|
|
#### Expansion Methods for Services (client-go compatible)
|
|
```go
|
|
// Start with a generic client for services
|
|
client, err := generic.NewClient[*corev1.Service](config)
|
|
|
|
// Get a namespace-scoped ServiceClient that implements typedcorev1.ServiceExpansion
|
|
serviceClient := client.ServiceClient("default") // Will panic if T is not *corev1.Service
|
|
|
|
// Use proxy to access service endpoints
|
|
req := serviceClient.ProxyGet("http", "my-service", "80", "api/health", nil)
|
|
resp, err := req.DoRaw(ctx)
|
|
```
|
|
|
|
#### Generic SubResource Access
|
|
```go
|
|
// Access any subresource using the generic method
|
|
req := client.SubResource("default", "my-pod", "status")
|
|
status, err := req.DoRaw(ctx)
|
|
```
|
|
|
|
#### Watch Resources
|
|
```go
|
|
// Watch for pod changes with label selector
|
|
watcher, err := client.Watch(ctx, "default", &metav1.ListOptions{
|
|
LabelSelector: "app=nginx",
|
|
})
|
|
|
|
defer watcher.Stop()
|
|
|
|
for event := range watcher.ResultChan() {
|
|
pod := event.Object.(*corev1.Pod)
|
|
fmt.Printf("Event: %s Pod: %s\n", event.Type, pod.Name)
|
|
}
|
|
```
|
|
|
|
#### Delete Collection
|
|
```go
|
|
// Delete all pods with specific label
|
|
err = client.DeleteCollection(ctx, "default", nil, &metav1.ListOptions{
|
|
LabelSelector: "app=test",
|
|
})
|
|
|
|
// Delete all pods in namespace
|
|
err = client.DeleteCollection(ctx, "default", nil, nil)
|
|
```
|
|
|
|
#### Update Status
|
|
```go
|
|
// Update only the status subresource
|
|
pod.Status.Phase = corev1.PodRunning
|
|
pod.Status.Conditions = append(pod.Status.Conditions, corev1.PodCondition{
|
|
Type: corev1.PodReady,
|
|
Status: corev1.ConditionTrue,
|
|
})
|
|
|
|
updated, err := client.UpdateStatus(ctx, "default", pod, nil)
|
|
```
|
|
|
|
## Testing
|
|
|
|
Unit tests against a mock REST client:
|
|
|
|
```
|
|
make test
|
|
```
|
|
|
|
End-to-end tests against a real K8s cluster:
|
|
|
|
```
|
|
make e2e
|
|
```
|
|
|
|
# ⚠️ THIS IS AN EXPERIMENT
|
|
|
|
This is just for demo purposes.
|
|
|
|
The name `client-go2` is a placeholder, and a joke.
|