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:
parent
d2b0d467e5
commit
e5071fc0cf
6 changed files with 445 additions and 64 deletions
2
Makefile
2
Makefile
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
206
main.go
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue