1
0
Fork 0
mirror of https://github.com/imjasonh/client-go2 synced 2026-07-18 06:37:34 +00:00

Update to use stable Go and add comprehensive CI/CD

- Replace gotip with stable Go in GitHub Actions
- Add automatic GVR inference for Kubernetes types
- Rename API: NewClient (inferred GVR) and NewClientGVR (explicit)
- Add comprehensive test coverage including e2e tests
- Update CI to include linting, unit tests, and e2e tests with kind
- Add .gitignore for common Go development files
- Update README with usage examples and features

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jason Hall 2025-07-23 11:41:20 -04:00
parent d6bb3cc61c
commit 74aace1a2d
Failed to extract signature
7 changed files with 554 additions and 122 deletions

View file

@ -3,23 +3,47 @@ name: Build
on: on:
push: push:
branches: ['main'] branches: ['main']
pull_request:
workflow_dispatch: workflow_dispatch:
jobs: jobs:
build: build:
name: build name: build
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- uses: actions/setup-go@v2
- uses: actions/setup-go@v5
with: with:
go-version: '1.17.x' go-version: 'stable'
- uses: engineerd/setup-kind@v0.5.0
- run: |
go install golang.org/dl/gotip@latest
gotip download
gotip build ./ - run: go build ./...
gotip run ./ - run: go vet ./...
gotip run ./
- run: |
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
echo "Code is not formatted. Run 'gofmt -s -w .'"
gofmt -s -d .
exit 1
fi
- run: |
go mod tidy
git diff --exit-code go.mod go.sum
- name: Run unit tests
run: go test ./... -v -race
- uses: chainguard-dev/actions/setup-kind@main
with:
k8s-version: v1.33.0
- run: |
kubectl cluster-info
kubectl get nodes
- name: Run e2e tests
run: go test ./... -v -tags=e2e
- name: Run example
run: go run ./main.go

36
.gitignore vendored Normal file
View file

@ -0,0 +1,36 @@
# Binaries
*.exe
*.exe~
*.dll
*.so
*.dylib
client-go2
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool
*.out
coverage.txt
# Go workspace file
go.work
go.work.sum
# Dependency directories
vendor/
# IDE specific files
.idea/
.vscode/
*.swp
*.swo
*~
# OS specific files
.DS_Store
Thumbs.db
# Temporary files
*.tmp
*.bak

View file

@ -4,31 +4,74 @@
This is an experimental type-parameter-aware client that wraps [`k8s.io/client-go/dynamic`](https://pkg.go.dev/k8s.io/client-go/dynamic) _(...for now)_. This is an experimental type-parameter-aware client that wraps [`k8s.io/client-go/dynamic`](https://pkg.go.dev/k8s.io/client-go/dynamic) _(...for now)_.
Assuming you've got a working kubeconfig (does `kubectl get pods` work?), you can run this code: ## Features
- **Type-safe generic client** - Work with strongly-typed Kubernetes objects instead of `unstructured.Unstructured`
- **Automatic GVR inference** - No need to manually specify GroupVersionResource for standard Kubernetes types
- **Full CRUD operations** - List, Get, Create, Update, Delete, and Patch support
- **Informer support** - Watch for changes with type-safe event handlers
- **Zero code generation** - Uses Go generics instead of code generation
## Usage
```go
// Create a client that automatically infers the GVR from the type
podClient, err := generic.NewClient[*corev1.Pod](config)
if err != nil {
log.Fatal(err)
}
// List pods in a namespace
pods, err := podClient.List(ctx, "kube-system")
if err != nil {
log.Fatal(err)
}
// Create a ConfigMap
cmClient, err := generic.NewClient[*corev1.ConfigMap](config)
if err != nil {
log.Fatal(err)
}
err = cmClient.Create(ctx, "default", &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "my-config",
},
Data: map[string]string{
"key": "value",
},
})
// If you need to specify a custom GVR, use NewClientGVR
customClient := generic.NewClientGVR[*corev1.Pod](customGVR, config)
```
## Running the Example
``` ```
$ go run ./ $ go run ./
2021/12/15 12:08:11 LISTING PODS 2025/07/23 11:23:10 LISTING PODS
2021/12/15 12:08:11 - coredns-558bd4d5db-hjs27 2025/07/23 11:23:10 - coredns-674b8bbfcf-ddcww
2021/12/15 12:08:11 - coredns-558bd4d5db-vhrtd 2025/07/23 11:23:10 - coredns-674b8bbfcf-dqqx5
2021/12/15 12:08:11 - etcd-kind-control-plane 2025/07/23 11:23:10 - etcd-kind-control-plane
2021/12/15 12:08:11 - kindnet-c977m 2025/07/23 11:23:10 - kindnet-tkf6l
2021/12/15 12:08:11 - kube-apiserver-kind-control-plane 2025/07/23 11:23:10 - kube-apiserver-kind-control-plane
2021/12/15 12:08:11 - kube-controller-manager-kind-control-plane 2025/07/23 11:23:10 - kube-controller-manager-kind-control-plane
2021/12/15 12:08:11 - kube-proxy-fgpfd 2025/07/23 11:23:10 - kube-proxy-76tcd
2021/12/15 12:08:11 - kube-scheduler-kind-control-plane 2025/07/23 11:23:10 - kube-scheduler-kind-control-plane
I1215 12:08:11.086322 32526 shared_informer.go:240] Waiting for caches to sync for /v1, Resource=configmaps
2021/12/15 12:08:11 --> ADD kube-public/cluster-info
2021/12/15 12:08:11 --> ADD kube-system/extension-apiserver-authentication
2021/12/15 12:08:11 --> ADD tekton-pipelines/config-logging
... ...
2021/12/15 12:08:11 LISTING CONFIGMAPS ```
2021/12/15 12:08:11 - coredns
2021/12/15 12:08:11 - extension-apiserver-authentication ## Testing
2021/12/15 12:08:11 - kube-proxy
2021/12/15 12:08:11 - kube-root-ca.crt Run unit tests:
2021/12/15 12:08:11 - kubeadm-config ```bash
2021/12/15 12:08:11 - kubelet-config-1.21 go test ./generic
```
Run e2e tests (requires a Kubernetes cluster):
```bash
go test ./generic -tags=e2e
``` ```
# THIS IS AN EXPERIMENT # THIS IS AN EXPERIMENT

View file

@ -4,7 +4,9 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"log" "log"
"reflect"
"time" "time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -12,28 +14,38 @@ import (
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/discovery"
"k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic"
"k8s.io/client-go/dynamic/dynamicinformer" "k8s.io/client-go/dynamic/dynamicinformer"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
"k8s.io/client-go/restmapper"
"k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/cache"
) )
const resyncPeriod = time.Hour const resyncPeriod = time.Hour
// Ideally, this wouldn't need to take a GVR, and it could just be inferred // NewClient creates a new generic client by automatically inferring
// from the runtime.Object type given. // the GroupVersionResource from the type parameter T.
// This uses the global Kubernetes scheme to look up the GVK for the type,
// then uses discovery to map that to a GVR.
// //
// In practice, this doesn't seem to be straightforward. :( // Note: T must be a pointer type (e.g., *corev1.Pod) as required by runtime.Object.
// Non-pointer types will fail at compile time.
func NewClient[T runtime.Object](config *rest.Config) (client[T], error) {
gvr, err := inferGVR[T](config)
if err != nil {
return client[T]{}, err
}
return NewClientGVR[T](gvr, config), nil
}
// NewClientGVR creates a new generic client with an explicit GroupVersionResource.
// This is useful when you need to specify a custom GVR or when the type isn't
// registered in the global scheme.
// //
// Things that implement runtime.Object tend to be pointer types (e.g., // Most users should prefer NewClient which automatically infers the GVR.
// *corev1.Pod), which means T is nil, and we can't call GetObjectKind() on it func NewClientGVR[T runtime.Object](gvr schema.GroupVersionResource, config *rest.Config) client[T] {
// to start to guess at the GVR.
//
// It might be possible to use schemes to lookup the GVR for a given Go type
// (assuming it's been registered), which could let us get rid of this.
// In the meantime, taking a GVR removes ambiguity at the cost of verbosity.
// /shrug
func NewClient[T runtime.Object](gvr schema.GroupVersionResource, config *rest.Config) client[T] {
dyn := dynamic.NewForConfigOrDie(config) dyn := dynamic.NewForConfigOrDie(config)
return client[T]{ return client[T]{
gvr: gvr, gvr: gvr,
@ -42,6 +54,68 @@ func NewClient[T runtime.Object](gvr schema.GroupVersionResource, config *rest.C
} }
} }
// inferGVR attempts to determine the GroupVersionResource for a given type T
// by using the Kubernetes scheme and discovery client.
func inferGVR[T runtime.Object](config *rest.Config) (schema.GroupVersionResource, error) {
// Create a zero-value instance of T to inspect
var zero T
typ := reflect.TypeOf(zero)
// Require pointer types - Kubernetes objects should always be pointers
if typ.Kind() != reflect.Ptr {
return schema.GroupVersionResource{}, fmt.Errorf("type %T must be a pointer type (e.g., *corev1.Pod, not corev1.Pod)", zero)
}
typ = typ.Elem()
// Create a new instance of the underlying type
instance := reflect.New(typ).Interface()
// Try to convert to runtime.Object
obj, ok := instance.(runtime.Object)
if !ok {
return schema.GroupVersionResource{}, fmt.Errorf("type %T does not implement runtime.Object", instance)
}
// Get the GVKs for this object from the scheme
gvks, _, err := scheme.Scheme.ObjectKinds(obj)
if err != nil {
return schema.GroupVersionResource{}, fmt.Errorf("failed to get GVK for type %T: %w", zero, err)
}
if len(gvks) == 0 {
return schema.GroupVersionResource{}, fmt.Errorf("no GVK registered for type %T", zero)
}
// If multiple match, return an error.
if len(gvks) > 1 {
return schema.GroupVersionResource{}, fmt.Errorf("multiple GVKs registered for type %T: %v", zero, gvks)
}
gvk := gvks[0]
// Create a discovery client to get the REST mapping
discoveryClient, err := discovery.NewDiscoveryClientForConfig(config)
if err != nil {
return schema.GroupVersionResource{}, fmt.Errorf("failed to create discovery client: %w", err)
}
// Get the API group resources
groupResources, err := restmapper.GetAPIGroupResources(discoveryClient)
if err != nil {
return schema.GroupVersionResource{}, fmt.Errorf("failed to get API group resources: %w", err)
}
// Create a REST mapper
mapper := restmapper.NewDiscoveryRESTMapper(groupResources)
// Get the resource mapping for the GVK
mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
if err != nil {
return schema.GroupVersionResource{}, fmt.Errorf("failed to get REST mapping for %v: %w", gvk, err)
}
return mapping.Resource, nil
}
type client[T runtime.Object] struct { type client[T runtime.Object] struct {
gvr schema.GroupVersionResource gvr schema.GroupVersionResource

View file

@ -12,10 +12,9 @@ import (
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/dynamic/fake" "k8s.io/client-go/dynamic/fake"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
"k8s.io/client-go/tools/cache"
) )
func TestNewClient(t *testing.T) { func TestNewClientGVR(t *testing.T) {
gvr := schema.GroupVersionResource{ gvr := schema.GroupVersionResource{
Group: "", Group: "",
Version: "v1", Version: "v1",
@ -23,7 +22,7 @@ func TestNewClient(t *testing.T) {
} }
config := &rest.Config{} config := &rest.Config{}
client := NewClient[*corev1.Pod](gvr, config) client := NewClientGVR[*corev1.Pod](gvr, config)
if client.gvr != gvr { if client.gvr != gvr {
t.Errorf("expected GVR %v, got %v", gvr, client.gvr) t.Errorf("expected GVR %v, got %v", gvr, client.gvr)
@ -347,64 +346,9 @@ func TestPatch(t *testing.T) {
} }
func TestInform(t *testing.T) { func TestInform(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background()) // Skip this test as it requires complex informer setup that doesn't work well with fake clients
defer cancel() // The informer functionality is better tested with e2e tests against a real cluster
t.Skip("Informer testing requires real cluster - see e2e tests")
namespace := "test-namespace"
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "inform-pod",
Namespace: namespace,
},
}
scheme := runtime.NewScheme()
if err := corev1.AddToScheme(scheme); err != nil {
t.Fatal(err)
}
dynClient := fake.NewSimpleDynamicClient(scheme, pod)
gvr := schema.GroupVersionResource{
Group: "",
Version: "v1",
Resource: "pods",
}
// Create client with informer factory
config := &rest.Config{}
client := NewClient[*corev1.Pod](gvr, config)
client.dyn = dynClient // Override with fake client for testing
// Track events
events := make(chan string, 10)
handler := cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
events <- "add"
},
UpdateFunc: func(oldObj, newObj interface{}) {
events <- "update"
},
DeleteFunc: func(obj interface{}) {
events <- "delete"
},
}
// Start informer
client.Start(ctx)
client.Inform(ctx, handler)
// Wait for initial sync
select {
case event := <-events:
if event != "add" {
t.Errorf("expected add event, got %s", event)
}
case <-ctx.Done():
t.Fatal("context cancelled before receiving event")
}
} }
// Test with ConfigMap to verify generic behavior // Test with ConfigMap to verify generic behavior
@ -455,3 +399,20 @@ func TestGenericWithConfigMap(t *testing.T) {
t.Errorf("expected key1=value1, got %s", configs[0].Data["key1"]) t.Errorf("expected key1=value1, got %s", configs[0].Data["key1"])
} }
} }
// TestNewClientGVRCustomResource tests using NewClientGVR with a custom GVR
func TestNewClientGVRCustomResource(t *testing.T) {
// Example: Using NewClientGVR for a custom resource that might not be in the scheme
customGVR := schema.GroupVersionResource{
Group: "custom.io",
Version: "v1",
Resource: "myresources",
}
config := &rest.Config{}
client := NewClientGVR[*corev1.Pod](customGVR, config) // Using Pod type as placeholder
if client.gvr != customGVR {
t.Errorf("expected custom GVR %v, got %v", customGVR, client.gvr)
}
}

300
generic/e2e_test.go Normal file
View file

@ -0,0 +1,300 @@
//go:build e2e
// +build e2e
package generic
import (
"context"
"testing"
"time"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/clientcmd"
)
// TestInferGVRE2E tests GVR inference against a real Kubernetes cluster
func TestInferGVRE2E(t *testing.T) {
config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
clientcmd.NewDefaultClientConfigLoadingRules(),
&clientcmd.ConfigOverrides{},
).ClientConfig()
if err != nil {
t.Fatalf("failed to load kubeconfig: %v", err)
}
tests := []struct {
name string
inferFunc func() (schema.GroupVersionResource, error)
expectedGVR schema.GroupVersionResource
}{
{
name: "Pod",
inferFunc: func() (schema.GroupVersionResource, error) {
return inferGVR[*corev1.Pod](config)
},
expectedGVR: schema.GroupVersionResource{
Group: "",
Version: "v1",
Resource: "pods",
},
},
{
name: "ConfigMap",
inferFunc: func() (schema.GroupVersionResource, error) {
return inferGVR[*corev1.ConfigMap](config)
},
expectedGVR: schema.GroupVersionResource{
Group: "",
Version: "v1",
Resource: "configmaps",
},
},
{
name: "Service",
inferFunc: func() (schema.GroupVersionResource, error) {
return inferGVR[*corev1.Service](config)
},
expectedGVR: schema.GroupVersionResource{
Group: "",
Version: "v1",
Resource: "services",
},
},
{
name: "Secret",
inferFunc: func() (schema.GroupVersionResource, error) {
return inferGVR[*corev1.Secret](config)
},
expectedGVR: schema.GroupVersionResource{
Group: "",
Version: "v1",
Resource: "secrets",
},
},
{
name: "Namespace",
inferFunc: func() (schema.GroupVersionResource, error) {
return inferGVR[*corev1.Namespace](config)
},
expectedGVR: schema.GroupVersionResource{
Group: "",
Version: "v1",
Resource: "namespaces",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gvr, err := tt.inferFunc()
if err != nil {
t.Fatalf("failed to infer GVR: %v", err)
}
if gvr != tt.expectedGVR {
t.Errorf("expected GVR %v, got %v", tt.expectedGVR, gvr)
}
})
}
}
// TestNewClientE2E tests client creation with inferred GVR against a real cluster
func TestNewClientE2E(t *testing.T) {
config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
clientcmd.NewDefaultClientConfigLoadingRules(),
&clientcmd.ConfigOverrides{},
).ClientConfig()
if err != nil {
t.Fatalf("failed to load kubeconfig: %v", err)
}
ctx := context.Background()
t.Run("Pod client operations", func(t *testing.T) {
client, err := NewClient[*corev1.Pod](config)
if err != nil {
t.Fatalf("failed to create pod client: %v", err)
}
// Try to list pods in default namespace
pods, err := client.List(ctx, "default")
if err != nil {
t.Fatalf("failed to list pods: %v", err)
}
t.Logf("Successfully listed %d pods in default namespace", len(pods))
})
t.Run("ConfigMap client operations", func(t *testing.T) {
client, err := NewClient[*corev1.ConfigMap](config)
if err != nil {
t.Fatalf("failed to create configmap client: %v", err)
}
// Try to list configmaps in default namespace
cms, err := client.List(ctx, "default")
if err != nil {
t.Fatalf("failed to list configmaps: %v", err)
}
t.Logf("Successfully listed %d configmaps in default namespace", len(cms))
})
t.Run("Service client operations", func(t *testing.T) {
client, err := NewClient[*corev1.Service](config)
if err != nil {
t.Fatalf("failed to create service client: %v", err)
}
// Try to list services in default namespace
svcs, err := client.List(ctx, "default")
if err != nil {
t.Fatalf("failed to list services: %v", err)
}
t.Logf("Successfully listed %d services in default namespace", len(svcs))
})
}
// TestInferGVRErrorCases tests error cases for GVR inference
func TestInferGVRErrorCases(t *testing.T) {
config := &rest.Config{
Host: "http://localhost:8080",
}
t.Run("Unregistered type", func(t *testing.T) {
// This should fail because no scheme is registered for this type
type UnregisteredType struct {
*corev1.Pod
}
_, err := inferGVR[*UnregisteredType](config)
if err == nil {
t.Error("expected error for unregistered type, got nil")
}
})
}
// TestInformE2E tests informer functionality against a real cluster
func TestInformE2E(t *testing.T) {
config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
clientcmd.NewDefaultClientConfigLoadingRules(),
&clientcmd.ConfigOverrides{},
).ClientConfig()
if err != nil {
t.Fatalf("failed to load kubeconfig: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Create a ConfigMap client with inferred GVR
client, err := NewClient[*corev1.ConfigMap](config)
if err != nil {
t.Fatalf("failed to create configmap client: %v", err)
}
// Start the informer
client.Start(ctx)
// Track events
events := make(chan string, 100)
handler := cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
// The dynamic informer returns unstructured objects
meta, err := cache.MetaNamespaceKeyFunc(obj)
if err != nil {
return
}
namespace, name, _ := cache.SplitMetaNamespaceKey(meta)
if namespace == "default" {
events <- "add:" + name
}
},
UpdateFunc: func(oldObj, newObj interface{}) {
meta, err := cache.MetaNamespaceKeyFunc(newObj)
if err != nil {
return
}
namespace, name, _ := cache.SplitMetaNamespaceKey(meta)
if namespace == "default" {
events <- "update:" + name
}
},
DeleteFunc: func(obj interface{}) {
meta, err := cache.MetaNamespaceKeyFunc(obj)
if err != nil {
return
}
namespace, name, _ := cache.SplitMetaNamespaceKey(meta)
if namespace == "default" {
events <- "delete:" + name
}
},
}
client.Inform(ctx, handler)
// Create a test ConfigMap
testCM := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-inform-" + time.Now().Format("20060102-150405"),
Namespace: "default",
},
Data: map[string]string{
"test": "data",
},
}
if err := client.Create(ctx, "default", testCM); err != nil {
t.Fatalf("failed to create test configmap: %v", err)
}
defer func() {
// Clean up
if err := client.Delete(ctx, "default", testCM.Name); err != nil {
t.Logf("failed to delete test configmap: %v", err)
}
}()
// Wait for add event for our specific ConfigMap
deadline := time.After(10 * time.Second)
foundAdd := false
for !foundAdd {
select {
case event := <-events:
t.Logf("Received event: %s", event)
if event == "add:"+testCM.Name {
foundAdd = true
}
case <-deadline:
t.Fatal("timeout waiting for add event")
}
}
// Update the ConfigMap
testCM.Data["test"] = "updated"
if err := client.Update(ctx, "default", testCM); err != nil {
t.Fatalf("failed to update test configmap: %v", err)
}
// Wait for update event
deadline = time.After(10 * time.Second)
foundUpdate := false
for !foundUpdate {
select {
case event := <-events:
t.Logf("Received event: %s", event)
if event == "update:"+testCM.Name {
foundUpdate = true
}
case <-deadline:
t.Fatal("timeout waiting for update event")
}
}
}

28
main.go
View file

@ -7,24 +7,10 @@ import (
"github.com/imjasonh/client-go2/generic" "github.com/imjasonh/client-go2/generic"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd"
) )
var (
podGVR = schema.GroupVersionResource{
Group: "",
Version: "v1",
Resource: "pods",
}
cmGVR = schema.GroupVersionResource{
Group: "",
Version: "v1",
Resource: "configmaps",
}
)
func main() { func main() {
ctx := context.Background() ctx := context.Background()
config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(clientcmd.NewDefaultClientConfigLoadingRules(), &clientcmd.ConfigOverrides{}).ClientConfig() config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(clientcmd.NewDefaultClientConfigLoadingRules(), &clientcmd.ConfigOverrides{}).ClientConfig()
@ -32,8 +18,12 @@ func main() {
log.Fatalf("ClientConfig: %v", err) log.Fatalf("ClientConfig: %v", err)
} }
// List pods in kube-system. // List pods in kube-system using automatic GVR inference.
pods, err := generic.NewClient[*corev1.Pod](podGVR, config).List(ctx, "kube-system") podClient, err := generic.NewClient[*corev1.Pod](config)
if err != nil {
log.Fatal("creating pod client:", err)
}
pods, err := podClient.List(ctx, "kube-system")
if err != nil { if err != nil {
log.Fatal("listing pods:", err) log.Fatal("listing pods:", err)
} }
@ -42,7 +32,11 @@ func main() {
log.Println("-", p.Name) log.Println("-", p.Name)
} }
cmc := generic.NewClient[*corev1.ConfigMap](cmGVR, config) // For ConfigMaps, we'll also use automatic GVR inference
cmc, err := generic.NewClient[*corev1.ConfigMap](config)
if err != nil {
log.Fatal("creating configmap client:", err)
}
cmc.Start(ctx) cmc.Start(ctx)
// Start an informer to log all adds/updates/deletes for ConfigMaps. // Start an informer to log all adds/updates/deletes for ConfigMaps.