From e5071fc0cfa1c00c9dc679b7ee64ae55e6a19bf2 Mon Sep 17 00:00:00 2001 From: Jason Hall Date: Sun, 27 Jul 2025 17:13:22 -0400 Subject: [PATCH] Add support for label and field selectors in informers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- Makefile | 2 +- README.md | 2 +- generic/client.go | 88 ++++++++++++++---- generic/client_test.go | 197 +++++++++++++++++++++++++++++++++++++-- generic/e2e_test.go | 14 +-- main.go | 206 +++++++++++++++++++++++++++++++++++------ 6 files changed, 445 insertions(+), 64 deletions(-) diff --git a/Makefile b/Makefile index 3562b76..5cfac18 100644 --- a/Makefile +++ b/Makefile @@ -19,4 +19,4 @@ fmt: lint: golangci-lint run --fix ./... -all: tidy test vet fmt lint e2e example +all: tidy vet fmt lint test e2e example diff --git a/README.md b/README.md index 6b4c76f..c344b0d 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ if err != nil { log.Fatal(err) } -err = cmClient.Create(ctx, "default", &corev1.ConfigMap{ +cm, err = cmClient.Create(ctx, "default", &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: "my-config", }, diff --git a/generic/client.go b/generic/client.go index 2b7130a..7db7035 100644 --- a/generic/client.go +++ b/generic/client.go @@ -144,12 +144,15 @@ type Client[T runtime.Object] struct { } // 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 body, err := c.restClient.Get(). NamespaceIfScoped(namespace, namespace != ""). Resource(c.gvr.Resource). - VersionedParams(&metav1.ListOptions{}, scheme.ParameterCodec). + VersionedParams(opts, scheme.ParameterCodec). Do(ctx). Raw() 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. -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 body, err := c.restClient.Get(). NamespaceIfScoped(namespace, namespace != ""). Resource(c.gvr.Resource). Name(name). - VersionedParams(&metav1.GetOptions{}, scheme.ParameterCodec). + VersionedParams(opts, scheme.ParameterCodec). Do(ctx). Raw() 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. -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(). NamespaceIfScoped(namespace, namespace != ""). Resource(c.gvr.Resource). - VersionedParams(&metav1.CreateOptions{}, scheme.ParameterCodec). + VersionedParams(opts, scheme.ParameterCodec). Body(t). Do(ctx). 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. -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 data, err := json.Marshal(t) if err != nil { @@ -245,7 +257,7 @@ func (c Client[T]) Update(ctx context.Context, namespace string, t T) (T, error) NamespaceIfScoped(namespace, namespace != ""). Resource(c.gvr.Resource). Name(meta.Name). - VersionedParams(&metav1.UpdateOptions{}, scheme.ParameterCodec). + VersionedParams(opts, scheme.ParameterCodec). Body(t). Do(ctx). 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. -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(). NamespaceIfScoped(namespace, namespace != ""). Resource(c.gvr.Resource). Name(name). - VersionedParams(&metav1.DeleteOptions{}, scheme.ParameterCodec). + VersionedParams(opts, scheme.ParameterCodec). Do(ctx). Error() } // 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). NamespaceIfScoped(namespace, namespace != ""). Resource(c.gvr.Resource). Name(name). - VersionedParams(&metav1.PatchOptions{}, scheme.ParameterCodec). + VersionedParams(opts, scheme.ParameterCodec). Body(data). Do(ctx). Raw() @@ -298,28 +316,60 @@ type InformerHandler[T runtime.Object] struct { 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 -func (c Client[T]) Inform(ctx context.Context, handler InformerHandler[T]) { - // Create a ListWatch using rest.Client +func (c Client[T]) Inform(ctx context.Context, handler InformerHandler[T], opts *InformOptions) { + // Create a ListWatch using rest.Client with label selector support 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(). Resource(c.gvr.Resource). - VersionedParams(&options, scheme.ParameterCodec). + VersionedParams(&listOpts, scheme.ParameterCodec). Do(ctx). 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(). Resource(c.gvr.Resource). - VersionedParams(&options, scheme.ParameterCodec). + VersionedParams(&watchOpts, scheme.ParameterCodec). Watch(ctx) }, } + // Set default resync period + resync := resyncPeriod + if opts != nil && opts.ResyncPeriod != nil { + resync = *opts.ResyncPeriod + } + // Create a new informer var zero T - informer := cache.NewSharedInformer(lw, zero, resyncPeriod) + informer := cache.NewSharedInformer(lw, zero, resync) _, err := informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: func(obj any) { diff --git a/generic/client_test.go b/generic/client_test.go index 3e42d8f..4fcbcaf 100644 --- a/generic/client_test.go +++ b/generic/client_test.go @@ -29,6 +29,9 @@ type mockResponse struct { func (m *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) { key := req.Method + " " + req.URL.Path + if req.URL.RawQuery != "" { + key += "?" + req.URL.RawQuery + } if resp, ok := m.responses[key]; ok { return &http.Response{ StatusCode: resp.statusCode, @@ -134,7 +137,7 @@ func TestList(t *testing.T) { restClient: restClient, } - pods, err := client.List(ctx, namespace) + pods, err := client.List(ctx, namespace, nil) if err != nil { t.Fatalf("List failed: %v", err) } @@ -214,7 +217,7 @@ func TestGet(t *testing.T) { restClient: restClient, } - pod, err := client.Get(ctx, namespace, podName) + pod, err := client.Get(ctx, namespace, podName, nil) if err != nil { t.Fatalf("Get failed: %v", err) } @@ -292,7 +295,7 @@ func TestCreate(t *testing.T) { restClient: restClient, } - created, err := client.Create(ctx, namespace, newPod) + created, err := client.Create(ctx, namespace, newPod, nil) if err != nil { t.Fatalf("Create failed: %v", err) } @@ -302,7 +305,7 @@ func TestCreate(t *testing.T) { } // 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 { t.Fatalf("Failed to get created pod: %v", err) } @@ -380,7 +383,7 @@ func TestUpdate(t *testing.T) { restClient: restClient, } - updated, err := client.Update(ctx, namespace, updatedPod) + updated, err := client.Update(ctx, namespace, updatedPod, nil) if err != nil { t.Fatalf("Update failed: %v", err) } @@ -390,7 +393,7 @@ func TestUpdate(t *testing.T) { } // Verify the update - result, err := client.Get(ctx, namespace, "update-pod") + result, err := client.Get(ctx, namespace, "update-pod", nil) if err != nil { t.Fatalf("Failed to get updated pod: %v", err) } @@ -440,7 +443,7 @@ func TestDelete(t *testing.T) { } // 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) } } @@ -515,7 +518,7 @@ func TestPatch(t *testing.T) { 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) } } @@ -586,7 +589,7 @@ func TestGenericWithConfigMap(t *testing.T) { } // Test List - configs, err := client.List(ctx, namespace) + configs, err := client.List(ctx, namespace, nil) if err != nil { 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) } } + +// 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) + } +} diff --git a/generic/e2e_test.go b/generic/e2e_test.go index 24548b8..29baae5 100644 --- a/generic/e2e_test.go +++ b/generic/e2e_test.go @@ -121,7 +121,7 @@ func TestNewClientE2E(t *testing.T) { } // Try to list pods in default namespace - pods, err := client.List(ctx, "default") + pods, err := client.List(ctx, "default", nil) if err != nil { t.Fatalf("failed to list pods: %v", err) } @@ -136,7 +136,7 @@ func TestNewClientE2E(t *testing.T) { } // Try to list configmaps in default namespace - cms, err := client.List(ctx, "default") + cms, err := client.List(ctx, "default", nil) if err != nil { t.Fatalf("failed to list configmaps: %v", err) } @@ -151,7 +151,7 @@ func TestNewClientE2E(t *testing.T) { } // Try to list services in default namespace - svcs, err := client.List(ctx, "default") + svcs, err := client.List(ctx, "default", nil) if err != nil { 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 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 { t.Fatalf("failed to create test configmap: %v", err) } testCM = created defer func() { // 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) } }() @@ -268,7 +268,7 @@ func TestInformE2E(t *testing.T) { // Update the ConfigMap testCM.Data["test"] = "updated" - updated, err := client.Update(ctx, "default", testCM) + updated, err := client.Update(ctx, "default", testCM, nil) if err != nil { t.Fatalf("failed to update test configmap: %v", err) } diff --git a/main.go b/main.go index e7d24b9..c98ac98 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "context" "log" + "time" "github.com/imjasonh/client-go2/generic" corev1 "k8s.io/api/core/v1" @@ -24,7 +25,7 @@ func main() { if err != nil { log.Fatal("creating pod client:", err) } - pods, err := podClient.List(ctx, "kube-system") + pods, err := podClient.List(ctx, "kube-system", nil) if err != nil { log.Fatal("listing pods:", err) } @@ -39,55 +40,206 @@ func main() { 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]{ 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) { - 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) { - 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) { - 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 - cm, err := cmc.Create(ctx, "kube-system", &corev1.ConfigMap{ + // Example 3: Start a Pod informer with field selector for running pods only + 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{ - GenerateName: "foo-", + GenerateName: "no-labels-", }, Data: map[string]string{ "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 { - 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 - cm.Data["hello"] = "universe" // Update the ConfigMap - log.Println("UPDATING CONFIGMAP", cm.Name) - _, 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") + // List all ConfigMaps in kube-system + log.Println("\nLISTING ALL CONFIGMAPS IN kube-system") + cms, err := cmc.List(ctx, "kube-system", nil) if err != nil { log.Fatal("listing configmaps:", err) } - log.Println("LISTING CONFIGMAPS") + log.Printf("Found %d ConfigMaps in kube-system:", len(cms)) for _, cm := range cms { log.Println("-", cm.Name) }