Files
kubemirror/pkg/discovery/discovery.go
T
2025-12-25 22:10:57 +00:00

154 lines
4.1 KiB
Go

// Package discovery provides automatic resource type discovery for Kubernetes clusters.
package discovery
import (
"context"
"fmt"
"strings"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/discovery"
"k8s.io/client-go/rest"
"github.com/lukaszraczylo/kubemirror/pkg/config"
)
// ResourceDiscovery discovers all mirrorable resource types in a cluster.
type ResourceDiscovery struct {
discoveryClient discovery.DiscoveryInterface
}
// NewResourceDiscovery creates a new resource discovery client.
func NewResourceDiscovery(cfg *rest.Config) (*ResourceDiscovery, error) {
dc, err := discovery.NewDiscoveryClientForConfig(cfg)
if err != nil {
return nil, fmt.Errorf("failed to create discovery client: %w", err)
}
return &ResourceDiscovery{
discoveryClient: dc,
}, nil
}
// DiscoverMirrorableResources discovers all resource types that can be mirrored.
// It filters out resources that shouldn't be mirrored based on a deny list.
func (d *ResourceDiscovery) DiscoverMirrorableResources(ctx context.Context) ([]config.ResourceType, error) {
// Get all API resources in the cluster
_, apiResourceLists, err := d.discoveryClient.ServerGroupsAndResources()
if err != nil {
// Partial errors are common (some APIs might not be fully available)
// Continue with what we have
if !discovery.IsGroupDiscoveryFailedError(err) {
return nil, fmt.Errorf("failed to discover API resources: %w", err)
}
}
var resources []config.ResourceType
seen := make(map[string]bool) // Deduplicate
for _, apiResourceList := range apiResourceLists {
gv, err := schema.ParseGroupVersion(apiResourceList.GroupVersion)
if err != nil {
continue
}
for _, apiResource := range apiResourceList.APIResources {
// Skip subresources (status, scale, etc.)
if strings.Contains(apiResource.Name, "/") {
continue
}
// Skip if not namespaced (we only mirror namespaced resources)
if !apiResource.Namespaced {
continue
}
// Skip if resource doesn't support required verbs
if !supportsRequiredVerbs(apiResource.Verbs) {
continue
}
// Skip denied resource types
if isDeniedResourceType(apiResource.Kind) {
continue
}
rt := config.ResourceType{
Group: gv.Group,
Version: gv.Version,
Kind: apiResource.Kind,
}
// Deduplicate by string representation
key := rt.String()
if seen[key] {
continue
}
seen[key] = true
resources = append(resources, rt)
}
}
return resources, nil
}
// supportsRequiredVerbs checks if a resource supports the verbs needed for mirroring.
func supportsRequiredVerbs(verbs metav1.Verbs) bool {
required := []string{"get", "list", "watch", "create", "update", "delete"}
verbSet := make(map[string]bool)
for _, v := range verbs {
verbSet[v] = true
}
for _, req := range required {
if !verbSet[req] {
return false
}
}
return true
}
// isDeniedResourceType checks if a resource type should never be mirrored.
var deniedKinds = map[string]bool{
// Kubernetes core resources that shouldn't be mirrored
"Pod": true,
"Node": true,
"Event": true,
"Endpoints": true,
"EndpointSlice": true,
"ComponentStatus": true,
"Binding": true,
"ReplicationController": true, // Deprecated, use Deployment
// Resources that are auto-generated or managed
"ControllerRevision": true,
"PodMetrics": true,
"NodeMetrics": true,
// Lease resources (used for leader election)
"Lease": true,
// CSI and storage resources
"CSIDriver": true,
"CSINode": true,
"CSIStorageCapacity": true,
"VolumeAttachment": true,
// Cluster-scoped resources that we filtered out but double-check
"Namespace": true,
"PersistentVolume": true,
"ClusterRole": true,
"ClusterRoleBinding": true,
"CustomResourceDefinition": true,
"APIService": true,
"ValidatingWebhookConfiguration": true,
"MutatingWebhookConfiguration": true,
}
func isDeniedResourceType(kind string) bool {
return deniedKinds[kind]
}