From e467e60335ff9b79d15d3d656579be5eaac5f2c9 Mon Sep 17 00:00:00 2001 From: Andrew Hay <39sumer3939@gmail.com> Date: Sun, 29 Jun 2025 16:58:30 -0400 Subject: [PATCH] chore(store*): add reduce complexity and improve code coverage (#5568) * chore(store*): add reduce complexity and improve code coverage * docs(store.go): reasoning for helper function * style(store): standardized order of args * chore: gofmt auto-format source/store.go for lint compliance --- .golangci.yml | 2 +- source/store.go | 466 +++++++++++++++++++++++++++++++------------ source/store_test.go | 34 ++++ 3 files changed, 368 insertions(+), 134 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index c67a5494e..d03e3cda8 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -40,7 +40,7 @@ linters: - name: confusing-naming disabled: true cyclop: # Lower cyclomatic complexity threshold after the max complexity is lowered - max-complexity: 51 + max-complexity: 44 testifylint: # Enable all checkers (https://github.com/Antonboom/testifylint#checkers). # Default: false diff --git a/source/store.go b/source/store.go index 82a03f1b9..cd458a0c8 100644 --- a/source/store.go +++ b/source/store.go @@ -45,6 +45,20 @@ import ( var ErrSourceNotFound = errors.New("source not found") // Config holds shared configuration options for all Sources. +// This struct centralizes all source-related configuration to avoid parameter proliferation +// in individual source constructors. It follows the configuration pattern where a single +// config object is passed rather than individual parameters. +// +// Common Configuration Fields: +// - Namespace: Target namespace for source operations +// - AnnotationFilter: Filter sources by annotation patterns +// - LabelFilter: Filter sources by label selectors +// - FQDNTemplate: Template for generating fully qualified domain names +// - CombineFQDNAndAnnotation: Whether to combine FQDN template with annotations +// - IgnoreHostnameAnnotation: Whether to ignore hostname annotations +// +// The config is created from externaldns.Config via NewSourceConfig() which handles +// type conversions and validation. type Config struct { Namespace string AnnotationFilter string @@ -135,7 +149,21 @@ func NewSourceConfig(cfg *externaldns.Config) *Config { } } -// ClientGenerator provides clients +// ClientGenerator provides clients for various Kubernetes APIs and external services. +// This interface abstracts client creation and enables dependency injection for testing. +// It uses the singleton pattern to ensure only one instance of each client is created +// and reused across multiple source instances. +// +// Supported Client Types: +// - KubeClient: Standard Kubernetes API client +// - GatewayClient: Gateway API client for Gateway resources +// - IstioClient: Istio service mesh client +// - CloudFoundryClient: CloudFoundry platform client +// - DynamicKubernetesClient: Dynamic client for custom resources +// - OpenShiftClient: OpenShift-specific client for Route resources +// +// The singleton behavior is implemented in SingletonClientGenerator which uses +// sync.Once to guarantee single initialization of each client type. type ClientGenerator interface { KubeClient() (kubernetes.Interface, error) GatewayClient() (gateway.Interface, error) @@ -145,8 +173,17 @@ type ClientGenerator interface { OpenShiftClient() (openshift.Interface, error) } -// SingletonClientGenerator stores provider clients and guarantees that only one instance of client -// will be generated +// SingletonClientGenerator stores provider clients and guarantees that only one instance of each client +// will be generated throughout the application lifecycle. +// +// Thread Safety: Uses sync.Once for each client type to ensure thread-safe initialization. +// This is important because external-dns may create multiple sources concurrently. +// +// Memory Efficiency: Prevents creating multiple instances of expensive client objects +// that maintain their own connection pools and caches. +// +// Configuration: Clients are configured using KubeConfig, APIServerURL, and RequestTimeout +// which are set during SingletonClientGenerator initialization. type SingletonClientGenerator struct { KubeConfig string APIServerURL string @@ -261,33 +298,47 @@ func ByNames(ctx context.Context, p ClientGenerator, names []string, cfg *Config return sources, nil } -// BuildWithConfig allows generating a Source implementation from the shared config +// BuildWithConfig creates a Source implementation using the factory pattern. +// This function serves as the central registry for all available source types. +// +// Source Selection: Uses a string identifier to determine which source type to create. +// This allows for runtime configuration and easy extension with new source types. +// +// Error Handling: Returns ErrSourceNotFound for unsupported source types, +// allowing callers to handle unknown sources gracefully. +// +// Supported Source Types: +// - "node": Kubernetes nodes +// - "service": Kubernetes services +// - "ingress": Kubernetes ingresses +// - "pod": Kubernetes pods +// - "gateway-*": Gateway API resources (httproute, grpcroute, tlsroute, tcproute, udproute) +// - "istio-*": Istio resources (gateway, virtualservice) +// - "cloudfoundry": CloudFoundry applications +// - "ambassador-host": Ambassador Host resources +// - "contour-httpproxy": Contour HTTPProxy resources +// - "gloo-proxy": Gloo proxy resources +// - "traefik-proxy": Traefik proxy resources +// - "openshift-route": OpenShift Route resources +// - "crd": Custom Resource Definitions +// - "skipper-routegroup": Skipper RouteGroup resources +// - "kong-tcpingress": Kong TCP Ingress resources +// - "f5-*": F5 resources (virtualserver, transportserver) +// - "fake": Fake source for testing +// - "connector": Connector source for external systems +// +// Design Note: Gateway API sources use a different pattern (direct constructor calls) +// because they have simpler initialization requirements. func BuildWithConfig(ctx context.Context, source string, p ClientGenerator, cfg *Config) (Source, error) { switch source { case "node": - client, err := p.KubeClient() - if err != nil { - return nil, err - } - return NewNodeSource(ctx, client, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.LabelFilter, cfg.ExposeInternalIPv6, cfg.ExcludeUnschedulable, cfg.CombineFQDNAndAnnotation) + return buildNodeSource(ctx, p, cfg) case "service": - client, err := p.KubeClient() - if err != nil { - return nil, err - } - return NewServiceSource(ctx, client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.Compatibility, cfg.PublishInternal, cfg.PublishHostIP, cfg.AlwaysPublishNotReadyAddresses, cfg.ServiceTypeFilter, cfg.IgnoreHostnameAnnotation, cfg.LabelFilter, cfg.ResolveLoadBalancerHostname, cfg.ListenEndpointEvents, cfg.ExposeInternalIPv6) + return buildServiceSource(ctx, p, cfg) case "ingress": - client, err := p.KubeClient() - if err != nil { - return nil, err - } - return NewIngressSource(ctx, client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation, cfg.IgnoreIngressTLSSpec, cfg.IgnoreIngressRulesSpec, cfg.LabelFilter, cfg.IngressClassNames) + return buildIngressSource(ctx, p, cfg) case "pod": - client, err := p.KubeClient() - if err != nil { - return nil, err - } - return NewPodSource(ctx, client, cfg.Namespace, cfg.Compatibility, cfg.IgnoreNonHostNetworkPods, cfg.PodSourceDomain, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation) + return buildPodSource(ctx, p, cfg) case "gateway-httproute": return NewGatewayHTTPRouteSource(p, cfg) case "gateway-grpcroute": @@ -299,133 +350,274 @@ func BuildWithConfig(ctx context.Context, source string, p ClientGenerator, cfg case "gateway-udproute": return NewGatewayUDPRouteSource(p, cfg) case "istio-gateway": - kubernetesClient, err := p.KubeClient() - if err != nil { - return nil, err - } - istioClient, err := p.IstioClient() - if err != nil { - return nil, err - } - return NewIstioGatewaySource(ctx, kubernetesClient, istioClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation) + return buildIstioGatewaySource(ctx, p, cfg) case "istio-virtualservice": - kubernetesClient, err := p.KubeClient() - if err != nil { - return nil, err - } - istioClient, err := p.IstioClient() - if err != nil { - return nil, err - } - return NewIstioVirtualServiceSource(ctx, kubernetesClient, istioClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation) + return buildIstioVirtualServiceSource(ctx, p, cfg) case "cloudfoundry": - cfClient, err := p.CloudFoundryClient(cfg.CFAPIEndpoint, cfg.CFUsername, cfg.CFPassword) - if err != nil { - return nil, err - } - return NewCloudFoundrySource(cfClient) + return buildCloudFoundrySource(ctx, p, cfg) case "ambassador-host": - kubernetesClient, err := p.KubeClient() - if err != nil { - return nil, err - } - dynamicClient, err := p.DynamicKubernetesClient() - if err != nil { - return nil, err - } - return NewAmbassadorHostSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter, cfg.LabelFilter) + return buildAmbassadorHostSource(ctx, p, cfg) case "contour-httpproxy": - dynamicClient, err := p.DynamicKubernetesClient() - if err != nil { - return nil, err - } - return NewContourHTTPProxySource(ctx, dynamicClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation) + return buildContourHTTPProxySource(ctx, p, cfg) case "gloo-proxy": - kubernetesClient, err := p.KubeClient() - if err != nil { - return nil, err - } - dynamicClient, err := p.DynamicKubernetesClient() - if err != nil { - return nil, err - } - return NewGlooSource(dynamicClient, kubernetesClient, cfg.GlooNamespaces) + return buildGlooProxySource(ctx, p, cfg) case "traefik-proxy": - kubernetesClient, err := p.KubeClient() - if err != nil { - return nil, err - } - dynamicClient, err := p.DynamicKubernetesClient() - if err != nil { - return nil, err - } - return NewTraefikSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter, cfg.IgnoreHostnameAnnotation, cfg.TraefikDisableLegacy, cfg.TraefikDisableNew) + return buildTraefikProxySource(ctx, p, cfg) case "openshift-route": - ocpClient, err := p.OpenShiftClient() - if err != nil { - return nil, err - } - return NewOcpRouteSource(ctx, ocpClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation, cfg.LabelFilter, cfg.OCPRouterName) + return buildOpenShiftRouteSource(ctx, p, cfg) case "fake": return NewFakeSource(cfg.FQDNTemplate) case "connector": return NewConnectorSource(cfg.ConnectorServer) case "crd": - client, err := p.KubeClient() - if err != nil { - return nil, err - } - crdClient, scheme, err := NewCRDClientForAPIVersionKind(client, cfg.KubeConfig, cfg.APIServerURL, cfg.CRDSourceAPIVersion, cfg.CRDSourceKind) - if err != nil { - return nil, err - } - return NewCRDSource(crdClient, cfg.Namespace, cfg.CRDSourceKind, cfg.AnnotationFilter, cfg.LabelFilter, scheme, cfg.UpdateEvents) + return buildCRDSource(ctx, p, cfg) case "skipper-routegroup": - apiServerURL := cfg.APIServerURL - tokenPath := "" - token := "" - restConfig, err := GetRestConfig(cfg.KubeConfig, cfg.APIServerURL) - if err == nil { - apiServerURL = restConfig.Host - tokenPath = restConfig.BearerTokenFile - token = restConfig.BearerToken - } - return NewRouteGroupSource(cfg.RequestTimeout, token, tokenPath, apiServerURL, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.SkipperRouteGroupVersion, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation) + return buildSkipperRouteGroupSource(ctx, cfg) case "kong-tcpingress": - kubernetesClient, err := p.KubeClient() - if err != nil { - return nil, err - } - dynamicClient, err := p.DynamicKubernetesClient() - if err != nil { - return nil, err - } - return NewKongTCPIngressSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter, cfg.IgnoreHostnameAnnotation) + return buildKongTCPIngressSource(ctx, p, cfg) case "f5-virtualserver": - kubernetesClient, err := p.KubeClient() - if err != nil { - return nil, err - } - dynamicClient, err := p.DynamicKubernetesClient() - if err != nil { - return nil, err - } - return NewF5VirtualServerSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter) + return buildF5VirtualServerSource(ctx, p, cfg) case "f5-transportserver": - kubernetesClient, err := p.KubeClient() - if err != nil { - return nil, err - } - dynamicClient, err := p.DynamicKubernetesClient() - if err != nil { - return nil, err - } - return NewF5TransportServerSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter) + return buildF5TransportServerSource(ctx, p, cfg) } - return nil, ErrSourceNotFound } +// Source Builder Functions +// +// The following functions follow a standardized pattern for creating source instances. +// This standardization improves code consistency, maintainability, and readability. +// +// Standardized Function Signature Pattern: +// +// func buildXXXSource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) +// +// Standardized Constructor Parameter Pattern (where applicable): +// 1. ctx (context.Context) - Always first when supported by the source constructor +// 2. client(s) (kubernetes.Interface, dynamic.Interface, etc.) - Kubernetes clients +// 3. namespace (string) - Target namespace for the source +// 4. annotationFilter (string) - Filter for annotations +// 5. labelFilter (labels.Selector) - Filter for labels (when applicable) +// 6. fqdnTemplate (string) - FQDN template for DNS record generation +// 7. combineFQDNAndAnnotation (bool) - Whether to combine FQDN template with annotations +// 8. ...other parameters - Source-specific parameters in logical order +// +// Design Principles: +// - Each source type has its own specific requirements and dependencies +// - Separating build functions allows for clearer code organization and easier maintenance +// - Individual functions enable straightforward error handling and independent testing +// - Modularity makes it easier to add new source types or modify existing ones +// - Consistent parameter ordering reduces cognitive load when working with multiple sources +// +// Note: Some sources may deviate from the standard pattern due to their unique requirements +// (e.g., RouteGroupSource doesn't use ClientGenerator, GlooSource doesn't accept context) +// buildNodeSource creates a Node source for exposing node information as DNS records. +// Follows standard pattern: ctx, client, annotationFilter, fqdnTemplate, labelFilter, ...other +func buildNodeSource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) { + client, err := p.KubeClient() + if err != nil { + return nil, err + } + return NewNodeSource(ctx, client, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.LabelFilter, cfg.ExposeInternalIPv6, cfg.ExcludeUnschedulable, cfg.CombineFQDNAndAnnotation) +} + +// buildServiceSource creates a Service source for exposing Kubernetes services as DNS records. +// Follows standard pattern: ctx, client, namespace, annotationFilter, fqdnTemplate, ...other +func buildServiceSource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) { + client, err := p.KubeClient() + if err != nil { + return nil, err + } + return NewServiceSource(ctx, client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.Compatibility, cfg.PublishInternal, cfg.PublishHostIP, cfg.AlwaysPublishNotReadyAddresses, cfg.ServiceTypeFilter, cfg.IgnoreHostnameAnnotation, cfg.LabelFilter, cfg.ResolveLoadBalancerHostname, cfg.ListenEndpointEvents, cfg.ExposeInternalIPv6) +} + +// buildIngressSource creates an Ingress source for exposing Kubernetes ingresses as DNS records. +// Follows standard pattern: ctx, client, namespace, annotationFilter, fqdnTemplate, ...other +func buildIngressSource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) { + client, err := p.KubeClient() + if err != nil { + return nil, err + } + return NewIngressSource(ctx, client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation, cfg.IgnoreIngressTLSSpec, cfg.IgnoreIngressRulesSpec, cfg.LabelFilter, cfg.IngressClassNames) +} + +// buildPodSource creates a Pod source for exposing Kubernetes pods as DNS records. +// Follows standard pattern: ctx, client, namespace, ...other (no annotation/label filters) +func buildPodSource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) { + client, err := p.KubeClient() + if err != nil { + return nil, err + } + return NewPodSource(ctx, client, cfg.Namespace, cfg.Compatibility, cfg.IgnoreNonHostNetworkPods, cfg.PodSourceDomain, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation) +} + +// buildIstioGatewaySource creates an Istio Gateway source for exposing Istio gateways as DNS records. +// Requires both Kubernetes and Istio clients. Follows standard parameter pattern. +func buildIstioGatewaySource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) { + kubernetesClient, err := p.KubeClient() + if err != nil { + return nil, err + } + istioClient, err := p.IstioClient() + if err != nil { + return nil, err + } + return NewIstioGatewaySource(ctx, kubernetesClient, istioClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation) +} + +// buildIstioVirtualServiceSource creates an Istio VirtualService source for exposing virtual services as DNS records. +// Requires both Kubernetes and Istio clients. Follows standard parameter pattern. +func buildIstioVirtualServiceSource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) { + kubernetesClient, err := p.KubeClient() + if err != nil { + return nil, err + } + istioClient, err := p.IstioClient() + if err != nil { + return nil, err + } + return NewIstioVirtualServiceSource(ctx, kubernetesClient, istioClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation) +} + +// buildCloudFoundrySource creates a CloudFoundry source for exposing CF applications as DNS records. +// Uses CloudFoundry client instead of Kubernetes client. Simple constructor with minimal parameters. +func buildCloudFoundrySource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) { + cfClient, err := p.CloudFoundryClient(cfg.CFAPIEndpoint, cfg.CFUsername, cfg.CFPassword) + if err != nil { + return nil, err + } + return NewCloudFoundrySource(cfClient) +} + +func buildAmbassadorHostSource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) { + kubernetesClient, err := p.KubeClient() + if err != nil { + return nil, err + } + dynamicClient, err := p.DynamicKubernetesClient() + if err != nil { + return nil, err + } + return NewAmbassadorHostSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter, cfg.LabelFilter) +} + +func buildContourHTTPProxySource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) { + dynamicClient, err := p.DynamicKubernetesClient() + if err != nil { + return nil, err + } + return NewContourHTTPProxySource(ctx, dynamicClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation) +} + +// buildGlooProxySource creates a Gloo source for exposing Gloo proxies as DNS records. +// Requires both dynamic and standard Kubernetes clients. +// Note: Does not accept context parameter in constructor (legacy design). +func buildGlooProxySource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) { + kubernetesClient, err := p.KubeClient() + if err != nil { + return nil, err + } + dynamicClient, err := p.DynamicKubernetesClient() + if err != nil { + return nil, err + } + return NewGlooSource(dynamicClient, kubernetesClient, cfg.GlooNamespaces) +} + +func buildTraefikProxySource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) { + kubernetesClient, err := p.KubeClient() + if err != nil { + return nil, err + } + dynamicClient, err := p.DynamicKubernetesClient() + if err != nil { + return nil, err + } + return NewTraefikSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter, cfg.IgnoreHostnameAnnotation, cfg.TraefikDisableLegacy, cfg.TraefikDisableNew) +} + +func buildOpenShiftRouteSource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) { + ocpClient, err := p.OpenShiftClient() + if err != nil { + return nil, err + } + return NewOcpRouteSource(ctx, ocpClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation, cfg.LabelFilter, cfg.OCPRouterName) +} + +// buildCRDSource creates a CRD source for exposing custom resources as DNS records. +// Uses a specialized CRD client created via NewCRDClientForAPIVersionKind. +// Parameter order: crdClient, namespace, kind, annotationFilter, labelFilter, scheme, updateEvents +func buildCRDSource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) { + client, err := p.KubeClient() + if err != nil { + return nil, err + } + crdClient, scheme, err := NewCRDClientForAPIVersionKind(client, cfg.KubeConfig, cfg.APIServerURL, cfg.CRDSourceAPIVersion, cfg.CRDSourceKind) + if err != nil { + return nil, err + } + return NewCRDSource(crdClient, cfg.Namespace, cfg.CRDSourceKind, cfg.AnnotationFilter, cfg.LabelFilter, scheme, cfg.UpdateEvents) +} + +// buildSkipperRouteGroupSource creates a Skipper RouteGroup source for exposing route groups as DNS records. +// Special case: Does not use ClientGenerator pattern, instead manages its own authentication. +// Retrieves bearer token from REST config for API server authentication. +func buildSkipperRouteGroupSource(ctx context.Context, cfg *Config) (Source, error) { + apiServerURL := cfg.APIServerURL + tokenPath := "" + token := "" + restConfig, err := GetRestConfig(cfg.KubeConfig, cfg.APIServerURL) + if err == nil { + apiServerURL = restConfig.Host + tokenPath = restConfig.BearerTokenFile + token = restConfig.BearerToken + } + return NewRouteGroupSource(cfg.RequestTimeout, token, tokenPath, apiServerURL, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.SkipperRouteGroupVersion, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation) +} + +func buildKongTCPIngressSource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) { + kubernetesClient, err := p.KubeClient() + if err != nil { + return nil, err + } + dynamicClient, err := p.DynamicKubernetesClient() + if err != nil { + return nil, err + } + return NewKongTCPIngressSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter, cfg.IgnoreHostnameAnnotation) +} + +func buildF5VirtualServerSource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) { + kubernetesClient, err := p.KubeClient() + if err != nil { + return nil, err + } + dynamicClient, err := p.DynamicKubernetesClient() + if err != nil { + return nil, err + } + return NewF5VirtualServerSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter) +} + +func buildF5TransportServerSource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) { + kubernetesClient, err := p.KubeClient() + if err != nil { + return nil, err + } + dynamicClient, err := p.DynamicKubernetesClient() + if err != nil { + return nil, err + } + return NewF5TransportServerSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter) +} + +// instrumentedRESTConfig creates a REST config with request instrumentation for monitoring. +// Adds HTTP transport wrapper for Prometheus metrics collection and request timeout configuration. +// +// Path Processing: Simplifies URL paths for metrics by taking the last segment, +// reducing cardinality of metric labels for better performance. +// +// Timeout: Applies the specified request timeout to prevent hanging requests. func instrumentedRESTConfig(kubeConfig, apiServerURL string, requestTimeout time.Duration) (*rest.Config, error) { config, err := GetRestConfig(kubeConfig, apiServerURL) if err != nil { @@ -443,8 +635,16 @@ func instrumentedRESTConfig(kubeConfig, apiServerURL string, requestTimeout time return config, nil } -// GetRestConfig returns the rest clients config to get automatically -// data if you run inside a cluster or by passing flags. +// GetRestConfig returns the REST client configuration for Kubernetes API access. +// Supports both in-cluster and external cluster configurations. +// +// Configuration Priority: +// 1. If kubeConfig is empty, tries the recommended home file (~/.kube/config) +// 2. If kubeConfig is still empty, uses in-cluster service account +// 3. Otherwise, uses the specified kubeConfig file +// +// API Server Override: The apiServerURL parameter can override the server URL +// from the kubeconfig file, useful for proxy scenarios or custom endpoints. func GetRestConfig(kubeConfig, apiServerURL string) (*rest.Config, error) { if kubeConfig == "" { if _, err := os.Stat(clientcmd.RecommendedHomeFile); err == nil { diff --git a/source/store_test.go b/source/store_test.go index c245b0efa..3b9c38498 100644 --- a/source/store_test.go +++ b/source/store_test.go @@ -27,6 +27,7 @@ import ( "github.com/stretchr/testify/suite" istioclient "istio.io/client-go/pkg/clientset/versioned" istiofake "istio.io/client-go/pkg/clientset/versioned/fake" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" @@ -239,3 +240,36 @@ func (suite *ByNamesTestSuite) TestDynamicKubernetesClientFails() { func TestByNames(t *testing.T) { suite.Run(t, new(ByNamesTestSuite)) } + +type minimalMockClientGenerator struct{} + +var errMock = errors.New("mock not implemented") + +func (m *minimalMockClientGenerator) KubeClient() (kubernetes.Interface, error) { return nil, errMock } +func (m *minimalMockClientGenerator) GatewayClient() (gateway.Interface, error) { return nil, errMock } +func (m *minimalMockClientGenerator) IstioClient() (istioclient.Interface, error) { + return nil, errMock +} +func (m *minimalMockClientGenerator) CloudFoundryClient(string, string, string) (*cfclient.Client, error) { + return nil, errMock +} +func (m *minimalMockClientGenerator) DynamicKubernetesClient() (dynamic.Interface, error) { + return nil, errMock +} +func (m *minimalMockClientGenerator) OpenShiftClient() (openshift.Interface, error) { + return nil, errMock +} + +func TestBuildWithConfig_InvalidSource(t *testing.T) { + ctx := context.Background() + p := &minimalMockClientGenerator{} + cfg := &Config{LabelFilter: labels.NewSelector()} + + src, err := BuildWithConfig(ctx, "not-a-source", p, cfg) + if src != nil { + t.Errorf("expected nil source for invalid type, got: %v", src) + } + if !errors.Is(err, ErrSourceNotFound) { + t.Errorf("expected ErrSourceNotFound, got: %v", err) + } +}