1
0
Fork 0
mirror of https://github.com/imjasonh/client-go2 synced 2026-07-20 13:00:46 +00:00

Add support for label and field selectors in informers

- Add InformOptions struct with ListOptions and ResyncPeriod fields
- Update all client methods to accept options parameters
- Add comprehensive examples in main.go demonstrating:
  - Label selector filtering (test=example)
  - Field selector filtering (status.phase=Running)
  - Custom resync periods
- Add tests for label and field selectors with mock query parameter support
- Fix nil pointer handling in all client methods
- Update mock transport to handle query parameters correctly

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jason Hall 2025-07-27 17:13:22 -04:00
parent d2b0d467e5
commit e5071fc0cf
Failed to extract signature
6 changed files with 445 additions and 64 deletions

View file

@ -19,4 +19,4 @@ fmt:
lint: lint:
golangci-lint run --fix ./... golangci-lint run --fix ./...
all: tidy test vet fmt lint e2e example all: tidy vet fmt lint test e2e example

View file

@ -33,7 +33,7 @@ if err != nil {
log.Fatal(err) log.Fatal(err)
} }
err = cmClient.Create(ctx, "default", &corev1.ConfigMap{ cm, err = cmClient.Create(ctx, "default", &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: "my-config", Name: "my-config",
}, },

View file

@ -144,12 +144,15 @@ type Client[T runtime.Object] struct {
} }
// List retrieves a list of objects of type T from the specified namespace. // List retrieves a list of objects of type T from the specified namespace.
func (c Client[T]) List(ctx context.Context, namespace string) ([]T, error) { func (c Client[T]) List(ctx context.Context, namespace string, opts *metav1.ListOptions) ([]T, error) {
if opts == nil {
opts = &metav1.ListOptions{}
}
// Get raw response body // Get raw response body
body, err := c.restClient.Get(). body, err := c.restClient.Get().
NamespaceIfScoped(namespace, namespace != ""). NamespaceIfScoped(namespace, namespace != "").
Resource(c.gvr.Resource). Resource(c.gvr.Resource).
VersionedParams(&metav1.ListOptions{}, scheme.ParameterCodec). VersionedParams(opts, scheme.ParameterCodec).
Do(ctx). Do(ctx).
Raw() Raw()
if err != nil { if err != nil {
@ -176,13 +179,16 @@ func (c Client[T]) List(ctx context.Context, namespace string) ([]T, error) {
} }
// Get retrieves a single object of type T by name from the specified namespace. // Get retrieves a single object of type T by name from the specified namespace.
func (c Client[T]) Get(ctx context.Context, namespace, name string) (T, error) { func (c Client[T]) Get(ctx context.Context, namespace, name string, opts *metav1.GetOptions) (T, error) {
if opts == nil {
opts = &metav1.GetOptions{}
}
// Use Raw to get the bytes and unmarshal manually // Use Raw to get the bytes and unmarshal manually
body, err := c.restClient.Get(). body, err := c.restClient.Get().
NamespaceIfScoped(namespace, namespace != ""). NamespaceIfScoped(namespace, namespace != "").
Resource(c.gvr.Resource). Resource(c.gvr.Resource).
Name(name). Name(name).
VersionedParams(&metav1.GetOptions{}, scheme.ParameterCodec). VersionedParams(opts, scheme.ParameterCodec).
Do(ctx). Do(ctx).
Raw() Raw()
if err != nil { if err != nil {
@ -199,11 +205,14 @@ func (c Client[T]) Get(ctx context.Context, namespace, name string) (T, error) {
} }
// Create creates a new object of type T in the specified namespace. // Create creates a new object of type T in the specified namespace.
func (c Client[T]) Create(ctx context.Context, namespace string, t T) (T, error) { func (c Client[T]) Create(ctx context.Context, namespace string, t T, opts *metav1.CreateOptions) (T, error) {
if opts == nil {
opts = &metav1.CreateOptions{}
}
body, err := c.restClient.Post(). body, err := c.restClient.Post().
NamespaceIfScoped(namespace, namespace != ""). NamespaceIfScoped(namespace, namespace != "").
Resource(c.gvr.Resource). Resource(c.gvr.Resource).
VersionedParams(&metav1.CreateOptions{}, scheme.ParameterCodec). VersionedParams(opts, scheme.ParameterCodec).
Body(t). Body(t).
Do(ctx). Do(ctx).
Raw() Raw()
@ -221,7 +230,10 @@ func (c Client[T]) Create(ctx context.Context, namespace string, t T) (T, error)
} }
// Update updates an existing object of type T in the specified namespace. // Update updates an existing object of type T in the specified namespace.
func (c Client[T]) Update(ctx context.Context, namespace string, t T) (T, error) { func (c Client[T]) Update(ctx context.Context, namespace string, t T, opts *metav1.UpdateOptions) (T, error) {
if opts == nil {
opts = &metav1.UpdateOptions{}
}
// Extract the name from the object metadata // Extract the name from the object metadata
data, err := json.Marshal(t) data, err := json.Marshal(t)
if err != nil { if err != nil {
@ -245,7 +257,7 @@ func (c Client[T]) Update(ctx context.Context, namespace string, t T) (T, error)
NamespaceIfScoped(namespace, namespace != ""). NamespaceIfScoped(namespace, namespace != "").
Resource(c.gvr.Resource). Resource(c.gvr.Resource).
Name(meta.Name). Name(meta.Name).
VersionedParams(&metav1.UpdateOptions{}, scheme.ParameterCodec). VersionedParams(opts, scheme.ParameterCodec).
Body(t). Body(t).
Do(ctx). Do(ctx).
Raw() Raw()
@ -263,23 +275,29 @@ func (c Client[T]) Update(ctx context.Context, namespace string, t T) (T, error)
} }
// Delete deletes an object of type T by name from the specified namespace. // Delete deletes an object of type T by name from the specified namespace.
func (c Client[T]) Delete(ctx context.Context, namespace, name string) error { func (c Client[T]) Delete(ctx context.Context, namespace, name string, opts *metav1.DeleteOptions) error {
if opts == nil {
opts = &metav1.DeleteOptions{}
}
return c.restClient.Delete(). return c.restClient.Delete().
NamespaceIfScoped(namespace, namespace != ""). NamespaceIfScoped(namespace, namespace != "").
Resource(c.gvr.Resource). Resource(c.gvr.Resource).
Name(name). Name(name).
VersionedParams(&metav1.DeleteOptions{}, scheme.ParameterCodec). VersionedParams(opts, scheme.ParameterCodec).
Do(ctx). Do(ctx).
Error() Error()
} }
// Patch applies a patch to an object of type T in the specified namespace. // Patch applies a patch to an object of type T in the specified namespace.
func (c Client[T]) Patch(ctx context.Context, namespace, name string, pt types.PatchType, data []byte) error { func (c Client[T]) Patch(ctx context.Context, namespace, name string, pt types.PatchType, data []byte, opts *metav1.PatchOptions) error {
if opts == nil {
opts = &metav1.PatchOptions{}
}
_, err := c.restClient.Patch(pt). _, err := c.restClient.Patch(pt).
NamespaceIfScoped(namespace, namespace != ""). NamespaceIfScoped(namespace, namespace != "").
Resource(c.gvr.Resource). Resource(c.gvr.Resource).
Name(name). Name(name).
VersionedParams(&metav1.PatchOptions{}, scheme.ParameterCodec). VersionedParams(opts, scheme.ParameterCodec).
Body(data). Body(data).
Do(ctx). Do(ctx).
Raw() Raw()
@ -298,28 +316,60 @@ type InformerHandler[T runtime.Object] struct {
OnError func(obj any, err error) OnError func(obj any, err error)
} }
// InformOptions contains options for configuring an informer
type InformOptions struct {
// ListOptions allows setting label selectors, field selectors, etc.
ListOptions metav1.ListOptions
// ResyncPeriod overrides the default resync period if set
ResyncPeriod *time.Duration
}
// Inform starts an informer for the specified type T and calls the appropriate handler methods // Inform starts an informer for the specified type T and calls the appropriate handler methods
func (c Client[T]) Inform(ctx context.Context, handler InformerHandler[T]) { func (c Client[T]) Inform(ctx context.Context, handler InformerHandler[T], opts *InformOptions) {
// Create a ListWatch using rest.Client // Create a ListWatch using rest.Client with label selector support
lw := &cache.ListWatch{ lw := &cache.ListWatch{
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { ListFunc: func(listOpts metav1.ListOptions) (runtime.Object, error) {
// Merge provided options with runtime options
if opts != nil {
if opts.ListOptions.LabelSelector != "" {
listOpts.LabelSelector = opts.ListOptions.LabelSelector
}
if opts.ListOptions.FieldSelector != "" {
listOpts.FieldSelector = opts.ListOptions.FieldSelector
}
}
return c.restClient.Get(). return c.restClient.Get().
Resource(c.gvr.Resource). Resource(c.gvr.Resource).
VersionedParams(&options, scheme.ParameterCodec). VersionedParams(&listOpts, scheme.ParameterCodec).
Do(ctx). Do(ctx).
Get() Get()
}, },
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { WatchFunc: func(watchOpts metav1.ListOptions) (watch.Interface, error) {
// Merge provided options with runtime options
if opts != nil {
if opts.ListOptions.LabelSelector != "" {
watchOpts.LabelSelector = opts.ListOptions.LabelSelector
}
if opts.ListOptions.FieldSelector != "" {
watchOpts.FieldSelector = opts.ListOptions.FieldSelector
}
}
return c.restClient.Get(). return c.restClient.Get().
Resource(c.gvr.Resource). Resource(c.gvr.Resource).
VersionedParams(&options, scheme.ParameterCodec). VersionedParams(&watchOpts, scheme.ParameterCodec).
Watch(ctx) Watch(ctx)
}, },
} }
// Set default resync period
resync := resyncPeriod
if opts != nil && opts.ResyncPeriod != nil {
resync = *opts.ResyncPeriod
}
// Create a new informer // Create a new informer
var zero T var zero T
informer := cache.NewSharedInformer(lw, zero, resyncPeriod) informer := cache.NewSharedInformer(lw, zero, resync)
_, err := informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ _, err := informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj any) { AddFunc: func(obj any) {

View file

@ -29,6 +29,9 @@ type mockResponse struct {
func (m *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) { func (m *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
key := req.Method + " " + req.URL.Path key := req.Method + " " + req.URL.Path
if req.URL.RawQuery != "" {
key += "?" + req.URL.RawQuery
}
if resp, ok := m.responses[key]; ok { if resp, ok := m.responses[key]; ok {
return &http.Response{ return &http.Response{
StatusCode: resp.statusCode, StatusCode: resp.statusCode,
@ -134,7 +137,7 @@ func TestList(t *testing.T) {
restClient: restClient, restClient: restClient,
} }
pods, err := client.List(ctx, namespace) pods, err := client.List(ctx, namespace, nil)
if err != nil { if err != nil {
t.Fatalf("List failed: %v", err) t.Fatalf("List failed: %v", err)
} }
@ -214,7 +217,7 @@ func TestGet(t *testing.T) {
restClient: restClient, restClient: restClient,
} }
pod, err := client.Get(ctx, namespace, podName) pod, err := client.Get(ctx, namespace, podName, nil)
if err != nil { if err != nil {
t.Fatalf("Get failed: %v", err) t.Fatalf("Get failed: %v", err)
} }
@ -292,7 +295,7 @@ func TestCreate(t *testing.T) {
restClient: restClient, restClient: restClient,
} }
created, err := client.Create(ctx, namespace, newPod) created, err := client.Create(ctx, namespace, newPod, nil)
if err != nil { if err != nil {
t.Fatalf("Create failed: %v", err) t.Fatalf("Create failed: %v", err)
} }
@ -302,7 +305,7 @@ func TestCreate(t *testing.T) {
} }
// Verify the pod was created // Verify the pod was created
fetched, err := client.Get(ctx, namespace, "new-pod") fetched, err := client.Get(ctx, namespace, "new-pod", nil)
if err != nil { if err != nil {
t.Fatalf("Failed to get created pod: %v", err) t.Fatalf("Failed to get created pod: %v", err)
} }
@ -380,7 +383,7 @@ func TestUpdate(t *testing.T) {
restClient: restClient, restClient: restClient,
} }
updated, err := client.Update(ctx, namespace, updatedPod) updated, err := client.Update(ctx, namespace, updatedPod, nil)
if err != nil { if err != nil {
t.Fatalf("Update failed: %v", err) t.Fatalf("Update failed: %v", err)
} }
@ -390,7 +393,7 @@ func TestUpdate(t *testing.T) {
} }
// Verify the update // Verify the update
result, err := client.Get(ctx, namespace, "update-pod") result, err := client.Get(ctx, namespace, "update-pod", nil)
if err != nil { if err != nil {
t.Fatalf("Failed to get updated pod: %v", err) t.Fatalf("Failed to get updated pod: %v", err)
} }
@ -440,7 +443,7 @@ func TestDelete(t *testing.T) {
} }
// Delete the pod // Delete the pod
if err := client.Delete(ctx, namespace, "delete-pod"); err != nil { if err := client.Delete(ctx, namespace, "delete-pod", nil); err != nil {
t.Fatalf("Delete failed: %v", err) t.Fatalf("Delete failed: %v", err)
} }
} }
@ -515,7 +518,7 @@ func TestPatch(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
if err := client.Patch(ctx, namespace, "patch-pod", types.JSONPatchType, patchData); err != nil { if err := client.Patch(ctx, namespace, "patch-pod", types.JSONPatchType, patchData, nil); err != nil {
t.Fatalf("Patch failed: %v", err) t.Fatalf("Patch failed: %v", err)
} }
} }
@ -586,7 +589,7 @@ func TestGenericWithConfigMap(t *testing.T) {
} }
// Test List // Test List
configs, err := client.List(ctx, namespace) configs, err := client.List(ctx, namespace, nil)
if err != nil { if err != nil {
t.Fatalf("List ConfigMaps failed: %v", err) t.Fatalf("List ConfigMaps failed: %v", err)
} }
@ -622,3 +625,179 @@ func TestNewClientGVRCustomResource(t *testing.T) {
t.Errorf("expected custom GVR %v, got %v", customGVR, client.gvr) t.Errorf("expected custom GVR %v, got %v", customGVR, client.gvr)
} }
} }
// TestListWithLabelSelector tests List with label selector
func TestListWithLabelSelector(t *testing.T) {
ctx := context.Background()
namespace := "test-namespace"
pod1 := &corev1.Pod{
TypeMeta: metav1.TypeMeta{
Kind: "Pod",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "pod1",
Namespace: namespace,
Labels: map[string]string{
"app": "test",
},
},
}
// pod2 is defined to show what doesn't match the selector
_ = &corev1.Pod{
TypeMeta: metav1.TypeMeta{
Kind: "Pod",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "pod2",
Namespace: namespace,
Labels: map[string]string{
"app": "other",
},
},
}
podList := &corev1.PodList{
TypeMeta: metav1.TypeMeta{
Kind: "PodList",
APIVersion: "v1",
},
Items: []corev1.Pod{*pod1}, // Only pod1 matches the selector
}
listJSON, _ := json.Marshal(podList)
transport := &mockTransport{
responses: map[string]mockResponse{
"GET /api/v1/namespaces/test-namespace/pods?labelSelector=app%3Dtest": {
statusCode: 200,
body: string(listJSON),
},
},
}
config := &rest.Config{
Host: "http://localhost",
APIPath: "/api",
Transport: transport,
ContentConfig: rest.ContentConfig{
GroupVersion: &schema.GroupVersion{Version: "v1"},
NegotiatedSerializer: serializer.NewCodecFactory(runtime.NewScheme()).WithoutConversion(),
},
}
restClient, err := rest.RESTClientFor(config)
if err != nil {
t.Fatal(err)
}
gvr := schema.GroupVersionResource{
Group: "",
Version: "v1",
Resource: "pods",
}
client := Client[*corev1.Pod]{
gvr: gvr,
restClient: restClient,
}
// List with label selector
pods, err := client.List(ctx, namespace, &metav1.ListOptions{
LabelSelector: "app=test",
})
if err != nil {
t.Fatalf("List failed: %v", err)
}
if len(pods) != 1 {
t.Errorf("expected 1 pod, got %d", len(pods))
}
if pods[0].Name != "pod1" {
t.Errorf("expected pod1, got %s", pods[0].Name)
}
}
// TestListWithFieldSelector tests List with field selector
func TestListWithFieldSelector(t *testing.T) {
ctx := context.Background()
namespace := "test-namespace"
pod1 := &corev1.Pod{
TypeMeta: metav1.TypeMeta{
Kind: "Pod",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "running-pod",
Namespace: namespace,
},
Status: corev1.PodStatus{
Phase: corev1.PodRunning,
},
}
podList := &corev1.PodList{
TypeMeta: metav1.TypeMeta{
Kind: "PodList",
APIVersion: "v1",
},
Items: []corev1.Pod{*pod1},
}
listJSON, _ := json.Marshal(podList)
transport := &mockTransport{
responses: map[string]mockResponse{
"GET /api/v1/namespaces/test-namespace/pods?fieldSelector=status.phase%3DRunning": {
statusCode: 200,
body: string(listJSON),
},
},
}
config := &rest.Config{
Host: "http://localhost",
APIPath: "/api",
Transport: transport,
ContentConfig: rest.ContentConfig{
GroupVersion: &schema.GroupVersion{Version: "v1"},
NegotiatedSerializer: serializer.NewCodecFactory(runtime.NewScheme()).WithoutConversion(),
},
}
restClient, err := rest.RESTClientFor(config)
if err != nil {
t.Fatal(err)
}
gvr := schema.GroupVersionResource{
Group: "",
Version: "v1",
Resource: "pods",
}
client := Client[*corev1.Pod]{
gvr: gvr,
restClient: restClient,
}
// List with field selector
pods, err := client.List(ctx, namespace, &metav1.ListOptions{
FieldSelector: "status.phase=Running",
})
if err != nil {
t.Fatalf("List failed: %v", err)
}
if len(pods) != 1 {
t.Errorf("expected 1 pod, got %d", len(pods))
}
if pods[0].Name != "running-pod" {
t.Errorf("expected running-pod, got %s", pods[0].Name)
}
}

View file

@ -121,7 +121,7 @@ func TestNewClientE2E(t *testing.T) {
} }
// Try to list pods in default namespace // Try to list pods in default namespace
pods, err := client.List(ctx, "default") pods, err := client.List(ctx, "default", nil)
if err != nil { if err != nil {
t.Fatalf("failed to list pods: %v", err) t.Fatalf("failed to list pods: %v", err)
} }
@ -136,7 +136,7 @@ func TestNewClientE2E(t *testing.T) {
} }
// Try to list configmaps in default namespace // Try to list configmaps in default namespace
cms, err := client.List(ctx, "default") cms, err := client.List(ctx, "default", nil)
if err != nil { if err != nil {
t.Fatalf("failed to list configmaps: %v", err) t.Fatalf("failed to list configmaps: %v", err)
} }
@ -151,7 +151,7 @@ func TestNewClientE2E(t *testing.T) {
} }
// Try to list services in default namespace // Try to list services in default namespace
svcs, err := client.List(ctx, "default") svcs, err := client.List(ctx, "default", nil)
if err != nil { if err != nil {
t.Fatalf("failed to list services: %v", err) t.Fatalf("failed to list services: %v", err)
} }
@ -226,7 +226,7 @@ func TestInformE2E(t *testing.T) {
}, },
} }
client.Inform(ctx, handler) client.Inform(ctx, handler, nil)
// Create a test ConfigMap // Create a test ConfigMap
testCM := &corev1.ConfigMap{ testCM := &corev1.ConfigMap{
@ -239,14 +239,14 @@ func TestInformE2E(t *testing.T) {
}, },
} }
created, err := client.Create(ctx, "default", testCM) created, err := client.Create(ctx, "default", testCM, nil)
if err != nil { if err != nil {
t.Fatalf("failed to create test configmap: %v", err) t.Fatalf("failed to create test configmap: %v", err)
} }
testCM = created testCM = created
defer func() { defer func() {
// Clean up // Clean up
if err := client.Delete(ctx, "default", testCM.Name); err != nil { if err := client.Delete(ctx, "default", testCM.Name, nil); err != nil {
t.Logf("failed to delete test configmap: %v", err) t.Logf("failed to delete test configmap: %v", err)
} }
}() }()
@ -268,7 +268,7 @@ func TestInformE2E(t *testing.T) {
// Update the ConfigMap // Update the ConfigMap
testCM.Data["test"] = "updated" testCM.Data["test"] = "updated"
updated, err := client.Update(ctx, "default", testCM) updated, err := client.Update(ctx, "default", testCM, nil)
if err != nil { if err != nil {
t.Fatalf("failed to update test configmap: %v", err) t.Fatalf("failed to update test configmap: %v", err)
} }

206
main.go
View file

@ -3,6 +3,7 @@ package main
import ( import (
"context" "context"
"log" "log"
"time"
"github.com/imjasonh/client-go2/generic" "github.com/imjasonh/client-go2/generic"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
@ -24,7 +25,7 @@ func main() {
if err != nil { if err != nil {
log.Fatal("creating pod client:", err) log.Fatal("creating pod client:", err)
} }
pods, err := podClient.List(ctx, "kube-system") pods, err := podClient.List(ctx, "kube-system", nil)
if err != nil { if err != nil {
log.Fatal("listing pods:", err) log.Fatal("listing pods:", err)
} }
@ -39,55 +40,206 @@ func main() {
log.Fatal("creating configmap client:", err) log.Fatal("creating configmap client:", err)
} }
// Start an informer to log all adds/updates/deletes for ConfigMaps. // Example 1: Start an informer for ALL ConfigMaps (no selector)
log.Println("Starting informer for ALL ConfigMaps...")
cmc.Inform(ctx, generic.InformerHandler[*corev1.ConfigMap]{ cmc.Inform(ctx, generic.InformerHandler[*corev1.ConfigMap]{
OnAdd: func(key string, obj *corev1.ConfigMap) { OnAdd: func(key string, obj *corev1.ConfigMap) {
log.Printf("ConfigMap added: %s/%s", obj.Namespace, obj.Name) log.Printf("[ALL] ConfigMap added: %s/%s", obj.Namespace, obj.Name)
}, },
OnUpdate: func(key string, oldObj, newObj *corev1.ConfigMap) { OnUpdate: func(key string, oldObj, newObj *corev1.ConfigMap) {
log.Printf("ConfigMap updated: %s/%s (old: %s, new: %s)", oldObj.Namespace, oldObj.Name, oldObj.Data, newObj.Data) log.Printf("[ALL] ConfigMap updated: %s/%s", oldObj.Namespace, oldObj.Name)
}, },
OnDelete: func(key string, obj *corev1.ConfigMap) { OnDelete: func(key string, obj *corev1.ConfigMap) {
log.Printf("ConfigMap deleted: %s/%s", obj.Namespace, obj.Name) log.Printf("[ALL] ConfigMap deleted: %s/%s", obj.Namespace, obj.Name)
}, },
OnError: func(obj any, err error) { OnError: func(obj any, err error) {
log.Printf("Error in ConfigMap informer: %v (object: %v, type: %T)", err, obj, obj) log.Printf("[ALL] Error in ConfigMap informer: %v", err)
},
}, nil)
// Example 2: Start an informer with label selector
log.Println("Starting informer for ConfigMaps with label test=example...")
cmc.Inform(ctx, generic.InformerHandler[*corev1.ConfigMap]{
OnAdd: func(key string, obj *corev1.ConfigMap) {
log.Printf("[LABELED] ConfigMap added: %s/%s (labels: %v)", obj.Namespace, obj.Name, obj.Labels)
},
OnUpdate: func(key string, oldObj, newObj *corev1.ConfigMap) {
log.Printf("[LABELED] ConfigMap updated: %s/%s", oldObj.Namespace, oldObj.Name)
},
OnDelete: func(key string, obj *corev1.ConfigMap) {
log.Printf("[LABELED] ConfigMap deleted: %s/%s", obj.Namespace, obj.Name)
},
OnError: func(obj any, err error) {
log.Printf("[LABELED] Error: %v", err)
},
}, &generic.InformOptions{
ListOptions: metav1.ListOptions{
LabelSelector: "test=example",
}, },
}) })
// Create a ConfigMap // Example 3: Start a Pod informer with field selector for running pods only
cm, err := cmc.Create(ctx, "kube-system", &corev1.ConfigMap{ log.Println("Starting informer for Running Pods only...")
podClient.Inform(ctx, generic.InformerHandler[*corev1.Pod]{
OnAdd: func(key string, obj *corev1.Pod) {
log.Printf("[RUNNING] Pod added: %s/%s (phase: %s)", obj.Namespace, obj.Name, obj.Status.Phase)
},
OnUpdate: func(key string, oldObj, newObj *corev1.Pod) {
if oldObj.Status.Phase != newObj.Status.Phase {
log.Printf("[RUNNING] Pod phase changed: %s/%s (%s -> %s)",
oldObj.Namespace, oldObj.Name, oldObj.Status.Phase, newObj.Status.Phase)
}
},
OnDelete: func(key string, obj *corev1.Pod) {
log.Printf("[RUNNING] Pod deleted: %s/%s", obj.Namespace, obj.Name)
},
OnError: func(obj any, err error) {
log.Printf("[RUNNING] Error: %v", err)
},
}, &generic.InformOptions{
ListOptions: metav1.ListOptions{
FieldSelector: "status.phase=Running",
},
})
// Example 4: Informer with custom resync period
resync := 30 * time.Second
log.Printf("Starting informer with custom resync period of %v...\n", resync)
cmc.Inform(ctx, generic.InformerHandler[*corev1.ConfigMap]{
OnAdd: func(key string, obj *corev1.ConfigMap) {
log.Printf("[RESYNC] ConfigMap added: %s/%s", obj.Namespace, obj.Name)
},
OnUpdate: func(key string, oldObj, newObj *corev1.ConfigMap) {
log.Printf("[RESYNC] ConfigMap updated: %s/%s", oldObj.Namespace, oldObj.Name)
},
OnDelete: func(key string, obj *corev1.ConfigMap) {
log.Printf("[RESYNC] ConfigMap deleted: %s/%s", obj.Namespace, obj.Name)
},
OnError: func(obj any, err error) {
log.Printf("[RESYNC] Error: %v", err)
},
}, &generic.InformOptions{
ListOptions: metav1.ListOptions{
LabelSelector: "special=resync-test",
},
ResyncPeriod: &resync,
})
// Wait a moment for informers to sync
time.Sleep(2 * time.Second)
// Create ConfigMaps with different labels to demonstrate selectors
log.Println("\nCREATING TEST RESOURCES...")
// Create a ConfigMap without labels (will only show in ALL informer)
cm1, err := cmc.Create(ctx, "default", &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
GenerateName: "foo-", GenerateName: "no-labels-",
}, },
Data: map[string]string{ Data: map[string]string{
"hello": "world", "hello": "world",
}, },
}, nil)
if err != nil {
log.Printf("Error creating cm1: %v", err)
} else {
log.Printf("Created ConfigMap without labels: %s", cm1.Name)
}
// Create a ConfigMap with test=example label (will show in LABELED informer)
cm2, err := cmc.Create(ctx, "default", &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "labeled-",
Labels: map[string]string{
"test": "example",
},
},
Data: map[string]string{
"hello": "labeled",
},
}, nil)
if err != nil {
log.Printf("Error creating cm2: %v", err)
} else {
log.Printf("Created ConfigMap with test=example label: %s", cm2.Name)
}
// Create a ConfigMap with special=resync-test label (will show in RESYNC informer)
cm3, err := cmc.Create(ctx, "default", &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
GenerateName: "resync-test-",
Labels: map[string]string{
"special": "resync-test",
},
},
Data: map[string]string{
"hello": "resync",
},
}, nil)
if err != nil {
log.Printf("Error creating cm3: %v", err)
} else {
log.Printf("Created ConfigMap with special=resync-test label: %s", cm3.Name)
}
// Wait for create events
time.Sleep(2 * time.Second)
// Update the labeled ConfigMap
if cm2 != nil {
log.Printf("\nUPDATING CONFIGMAP %s", cm2.Name)
cm2.Data["hello"] = "updated"
_, err = cmc.Update(ctx, "default", cm2, nil)
if err != nil {
log.Printf("Error updating cm2: %v", err)
}
}
// Wait for update events
time.Sleep(2 * time.Second)
// Clean up
log.Println("\nCLEANING UP...")
if cm1 != nil {
if err := cmc.Delete(ctx, "default", cm1.Name, nil); err != nil {
log.Printf("Error deleting cm1: %v", err)
}
}
if cm2 != nil {
if err := cmc.Delete(ctx, "default", cm2.Name, nil); err != nil {
log.Printf("Error deleting cm2: %v", err)
}
}
if cm3 != nil {
if err := cmc.Delete(ctx, "default", cm3.Name, nil); err != nil {
log.Printf("Error deleting cm3: %v", err)
}
}
// Wait for delete events
time.Sleep(2 * time.Second)
// List ConfigMaps with label selector
log.Println("\nLISTING CONFIGMAPS WITH LABEL test=example")
labeledCMs, err := cmc.List(ctx, "", &metav1.ListOptions{
LabelSelector: "test=example",
}) })
if err != nil { if err != nil {
log.Fatal("creating configmap:", err) log.Printf("Error listing labeled configmaps: %v", err)
} else {
log.Printf("Found %d ConfigMaps with test=example label:", len(labeledCMs))
for _, cm := range labeledCMs {
log.Printf("- %s/%s", cm.Namespace, cm.Name)
}
} }
// Update the ConfigMap // List all ConfigMaps in kube-system
cm.Data["hello"] = "universe" // Update the ConfigMap log.Println("\nLISTING ALL CONFIGMAPS IN kube-system")
log.Println("UPDATING CONFIGMAP", cm.Name) cms, err := cmc.List(ctx, "kube-system", nil)
_, err = cmc.Update(ctx, "kube-system", cm)
if err != nil {
log.Fatal("updating configmap:", err)
}
// Delete the ConfigMap
log.Println("DELETING CONFIGMAP", cm.Name)
if err := cmc.Delete(ctx, "kube-system", cm.Name); err != nil {
log.Fatal("deleting configmap:", err)
}
// List ConfigMaps
cms, err := cmc.List(ctx, "kube-system")
if err != nil { if err != nil {
log.Fatal("listing configmaps:", err) log.Fatal("listing configmaps:", err)
} }
log.Println("LISTING CONFIGMAPS") log.Printf("Found %d ConfigMaps in kube-system:", len(cms))
for _, cm := range cms { for _, cm := range cms {
log.Println("-", cm.Name) log.Println("-", cm.Name)
} }