diff --git a/docs/flags.md b/docs/flags.md index 7e4abe145f..f7e8786267 100644 --- a/docs/flags.md +++ b/docs/flags.md @@ -9,6 +9,7 @@ | `--[no-]version` | Show application version. | | `--server=""` | The Kubernetes API server to connect to (default: auto-detect) | | `--kubeconfig=""` | Retrieve target cluster configuration from a Kubernetes configuration file (default: auto-detect) | +| `--kube-api-cache-sync-timeout=1m0s` | Timeout for waiting for Kubernetes informer caches to sync during startup. Values <= 0 use the default (60s). Increase only after ruling out RBAC, network, or API server issues. | | `--request-timeout=30s` | Request timeout when calling Kubernetes APIs. 0s means no timeout | | `--[no-]resolve-service-load-balancer-hostname` | Resolve the hostname of LoadBalancer-type Service object to IP addresses in order to create DNS A/AAAA records instead of CNAMEs | | `--[no-]listen-endpoint-events` | Trigger a reconcile on changes to EndpointSlices, for Service source (default: false) | diff --git a/mkdocs.yml b/mkdocs.yml index b4d92cc32d..0db10f466f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -71,9 +71,8 @@ markdown_extensions: - extra - admonition - smarty + - sane_lists - nl2br - - mdx_truly_sane_lists: - nested_indent: 2 - attr_list - def_list - footnotes diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index a4ce18bb5c..7eeac6d522 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -40,9 +40,10 @@ const ( // Config is a project-wide configuration type Config struct { - APIServerURL string - KubeConfig string - RequestTimeout time.Duration + APIServerURL string + KubeConfig string + CacheSyncTimeout time.Duration + RequestTimeout time.Duration DefaultTargets []string GlooNamespaces []string SkipperRouteGroupVersion string @@ -338,6 +339,7 @@ var defaultConfig = &Config{ RegexDomainExclude: regexp.MustCompile(""), RegexDomainFilter: regexp.MustCompile(""), Registry: "txt", + CacheSyncTimeout: time.Second * 60, RequestTimeout: time.Second * 30, RFC2136BatchChangeSize: 50, RFC2136GSSTSIG: false, @@ -493,6 +495,7 @@ func bindFlags(b flags.FlagBinder, cfg *Config) { // Flags related to Kubernetes b.StringVar("server", "The Kubernetes API server to connect to (default: auto-detect)", defaultConfig.APIServerURL, &cfg.APIServerURL) b.StringVar("kubeconfig", "Retrieve target cluster configuration from a Kubernetes configuration file (default: auto-detect)", defaultConfig.KubeConfig, &cfg.KubeConfig) + b.DurationVar("kube-api-cache-sync-timeout", "Timeout for waiting for Kubernetes informer caches to sync during startup. Values <= 0 use the default (60s). Increase only after ruling out RBAC, network, or API server issues.", defaultConfig.CacheSyncTimeout, &cfg.CacheSyncTimeout) b.DurationVar("request-timeout", "Request timeout when calling Kubernetes APIs. 0s means no timeout", defaultConfig.RequestTimeout, &cfg.RequestTimeout) b.BoolVar("resolve-service-load-balancer-hostname", "Resolve the hostname of LoadBalancer-type Service object to IP addresses in order to create DNS A/AAAA records instead of CNAMEs", false, &cfg.ResolveServiceLoadBalancerHostname) b.BoolVar("listen-endpoint-events", "Trigger a reconcile on changes to EndpointSlices, for Service source (default: false)", false, &cfg.ListenEndpointEvents) diff --git a/pkg/apis/externaldns/types_test.go b/pkg/apis/externaldns/types_test.go index 34bb4a7ae2..cee5d194ad 100644 --- a/pkg/apis/externaldns/types_test.go +++ b/pkg/apis/externaldns/types_test.go @@ -36,6 +36,7 @@ var ( minimalConfig = &Config{ APIServerURL: "", KubeConfig: "", + CacheSyncTimeout: time.Second * 60, RequestTimeout: time.Second * 30, GlooNamespaces: []string{"gloo-system"}, SkipperRouteGroupVersion: "zalando.org/v1", @@ -139,6 +140,7 @@ var ( overriddenConfig = &Config{ APIServerURL: "http://127.0.0.1:8080", KubeConfig: "/some/path", + CacheSyncTimeout: time.Second * 60, RequestTimeout: time.Second * 77, GlooNamespaces: []string{"gloo-not-system", "gloo-second-system"}, SkipperRouteGroupVersion: "zalando.org/v2", diff --git a/source/ambassador_host.go b/source/ambassador_host.go index 730c05385f..2af50b995b 100644 --- a/source/ambassador_host.go +++ b/source/ambassador_host.go @@ -22,6 +22,7 @@ import ( "fmt" "sort" "strings" + "time" ambassador "github.com/datawire/ambassador/pkg/api/getambassador.io/v2" log "github.com/sirupsen/logrus" @@ -83,6 +84,7 @@ func NewAmbassadorHostSource( namespace string, annotationFilter string, labelSelector labels.Selector, + timeout time.Duration, ) (Source, error) { // Use shared informer to listen for add/update/delete of Host in the specified namespace. // Set resync period to 0, to prevent processing when nothing has changed. @@ -100,7 +102,7 @@ func NewAmbassadorHostSource( informerFactory.Start(ctx.Done()) // wait for the local cache to be populated. - if err := informers.WaitForDynamicCacheSync(ctx, informerFactory); err != nil { + if err := informers.WaitForDynamicCacheSync(ctx, informerFactory, timeout); err != nil { return nil, err } diff --git a/source/ambassador_host_test.go b/source/ambassador_host_test.go index 1f9e84664a..be7d36e86c 100644 --- a/source/ambassador_host_test.go +++ b/source/ambassador_host_test.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "testing" + "time" ambassador "github.com/datawire/ambassador/pkg/api/getambassador.io/v2" "github.com/stretchr/testify/assert" @@ -637,7 +638,7 @@ func TestAmbassadorHostSource(t *testing.T) { _, err = fakeDynamicClient.Resource(ambHostGVR).Namespace(namespace).Create(context.Background(), host, metav1.CreateOptions{}) assert.NoError(t, err) - source, err := NewAmbassadorHostSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, namespace, ti.annotationFilter, ti.labelSelector) + source, err := NewAmbassadorHostSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, namespace, ti.annotationFilter, ti.labelSelector, time.Duration(0)) assert.NoError(t, err) assert.NotNil(t, source) diff --git a/source/contour_httpproxy.go b/source/contour_httpproxy.go index 7697204932..2b2698eadc 100644 --- a/source/contour_httpproxy.go +++ b/source/contour_httpproxy.go @@ -22,6 +22,7 @@ import ( "fmt" "sort" "text/template" + "time" projectcontour "github.com/projectcontour/contour/apis/projectcontour/v1" log "github.com/sirupsen/logrus" @@ -69,6 +70,7 @@ func NewContourHTTPProxySource( fqdnTemplate string, combineFqdnAnnotation bool, ignoreHostnameAnnotation bool, + timeout time.Duration, ) (Source, error) { tmpl, err := fqdn.ParseTemplate(fqdnTemplate) if err != nil { @@ -91,7 +93,7 @@ func NewContourHTTPProxySource( informerFactory.Start(ctx.Done()) // wait for the local cache to be populated. - if err := informers.WaitForDynamicCacheSync(ctx, informerFactory); err != nil { + if err := informers.WaitForDynamicCacheSync(ctx, informerFactory, timeout); err != nil { return nil, err } diff --git a/source/contour_httpproxy_test.go b/source/contour_httpproxy_test.go index 15a45b2ddd..ad23dbdde1 100644 --- a/source/contour_httpproxy_test.go +++ b/source/contour_httpproxy_test.go @@ -20,6 +20,7 @@ import ( "context" "errors" "testing" + "time" fakeDynamic "k8s.io/client-go/dynamic/fake" @@ -97,6 +98,7 @@ func (suite *HTTPProxySuite) SetupTest() { "{{.Name}}", false, false, + time.Duration(0), ) suite.NoError(err, "should initialize httpproxy source") @@ -194,6 +196,7 @@ func TestNewContourHTTPProxySource(t *testing.T) { ti.fqdnTemplate, ti.combineFQDNAndAnnotation, false, + time.Duration(0), ) if ti.expectError { assert.Error(t, err) @@ -1062,6 +1065,7 @@ func testHTTPProxyEndpoints(t *testing.T) { ti.fqdnTemplate, ti.combineFQDNAndAnnotation, ti.ignoreHostnameAnnotation, + time.Duration(0), ) require.NoError(t, err) @@ -1089,6 +1093,7 @@ func newTestHTTPProxySource() (*httpProxySource, error) { "{{.Name}}", false, false, + time.Duration(0), ) if err != nil { return nil, err diff --git a/source/crd.go b/source/crd.go index 2b2a172afb..c876d45035 100644 --- a/source/crd.go +++ b/source/crd.go @@ -21,12 +21,14 @@ import ( "fmt" "os" "strings" + "time" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/tools/cache" "sigs.k8s.io/external-dns/source/annotations" + "sigs.k8s.io/external-dns/source/informers" log "github.com/sirupsen/logrus" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -63,7 +65,7 @@ type crdSource struct { } // NewCRDClientForAPIVersionKind return rest client for the given apiVersion and kind of the CRD -func NewCRDClientForAPIVersionKind(client kubernetes.Interface, kubeConfig, apiServerURL, apiVersion, kind string) (*rest.RESTClient, *runtime.Scheme, error) { +func NewCRDClientForAPIVersionKind(client kubernetes.Interface, kubeConfig, apiServerURL, apiVersion, kind string, timeout time.Duration) (*rest.RESTClient, *runtime.Scheme, error) { if kubeConfig == "" { if _, err := os.Stat(clientcmd.RecommendedHomeFile); err == nil { kubeConfig = clientcmd.RecommendedHomeFile @@ -101,6 +103,7 @@ func NewCRDClientForAPIVersionKind(client kubernetes.Interface, kubeConfig, apiS config.GroupVersion = &groupVersion config.APIPath = "/apis" config.NegotiatedSerializer = serializer.WithoutConversionCodecFactory{CodecFactory: serializer.NewCodecFactory(scheme)} + config.Timeout = timeout crdClient, err := rest.UnversionedRESTClientFor(config) if err != nil { @@ -115,7 +118,8 @@ func NewCRDSource( namespace, kind, annotationFilter string, labelSelector labels.Selector, scheme *runtime.Scheme, - startInformer bool) (Source, error) { + startInformer bool, + cacheSyncTimeout time.Duration) (Source, error) { sourceCrd := crdSource{ crdResource: strings.ToLower(kind) + "s", namespace: namespace, @@ -139,6 +143,16 @@ func NewCRDSource( &apiv1alpha1.DNSEndpoint{}, 0) go sourceCrd.informer.Run(wait.NeverStop) + + // Wait for the informer cache to sync before returning. + if cacheSyncTimeout <= 0 { + cacheSyncTimeout = time.Duration(informers.DefaultCacheSyncTimeout) * time.Second + } + ctx, cancel := context.WithTimeout(context.Background(), cacheSyncTimeout) + defer cancel() + if !cache.WaitForCacheSync(ctx.Done(), sourceCrd.informer.HasSynced) { + return nil, fmt.Errorf("CRD informer cache sync timed out after %s", cacheSyncTimeout) + } } return &sourceCrd, nil } diff --git a/source/crd_test.go b/source/crd_test.go index 4de29cf921..d5d70ffdc9 100644 --- a/source/crd_test.go +++ b/source/crd_test.go @@ -537,7 +537,7 @@ func testCRDSourceEndpoints(t *testing.T) { // At present, client-go's fake.RESTClient (used by crd_test.go) is known to cause race conditions when used // with informers: https://github.com/kubernetes/kubernetes/issues/95372 // So don't start the informer during testing. - cs, err := NewCRDSource(restClient, ti.namespace, ti.kind, ti.annotationFilter, labelSelector, scheme, false) + cs, err := NewCRDSource(restClient, ti.namespace, ti.kind, ti.annotationFilter, labelSelector, scheme, false, 0) require.NoError(t, err) receivedEndpoints, err := cs.Endpoints(t.Context()) diff --git a/source/endpoints.go b/source/endpoints.go index 107425b9ae..99c74fd07c 100644 --- a/source/endpoints.go +++ b/source/endpoints.go @@ -81,10 +81,6 @@ func EndpointsForHostname(hostname string, targets endpoint.Targets, ttl endpoin return endpoints } -// EndpointTargetsFromServices retrieves endpoint targets from services in a given namespace -// that match the specified selector. It returns external IPs or load balancer addresses. -// -// TODO: add support for service.Spec.Ports (type NodePort) and service.Spec.ClusterIPs (type ClusterIP) func EndpointTargetsFromServices(svcInformer coreinformers.ServiceInformer, namespace string, selector map[string]string) (endpoint.Targets, error) { targets := endpoint.Targets{} diff --git a/source/f5_transportserver.go b/source/f5_transportserver.go index 88f33a4517..59c965213a 100644 --- a/source/f5_transportserver.go +++ b/source/f5_transportserver.go @@ -21,6 +21,7 @@ import ( "errors" "fmt" "strings" + "time" log "github.com/sirupsen/logrus" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -72,6 +73,7 @@ func NewF5TransportServerSource( kubeClient kubernetes.Interface, namespace string, annotationFilter string, + timeout time.Duration, ) (Source, error) { informerFactory := dynamicinformer.NewFilteredDynamicSharedInformerFactory(dynamicKubeClient, 0, namespace, nil) transportServerInformer := informerFactory.ForResource(f5TransportServerGVR) @@ -86,7 +88,7 @@ func NewF5TransportServerSource( informerFactory.Start(ctx.Done()) // wait for the local cache to be populated. - if err := informers.WaitForDynamicCacheSync(ctx, informerFactory); err != nil { + if err := informers.WaitForDynamicCacheSync(ctx, informerFactory, timeout); err != nil { return nil, err } diff --git a/source/f5_transportserver_test.go b/source/f5_transportserver_test.go index 4418cefeed..2eb75bb71f 100644 --- a/source/f5_transportserver_test.go +++ b/source/f5_transportserver_test.go @@ -20,6 +20,7 @@ import ( "context" "encoding/json" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -345,7 +346,7 @@ func TestF5TransportServerEndpoints(t *testing.T) { _, err = fakeDynamicClient.Resource(f5TransportServerGVR).Namespace(defaultF5TransportServerNamespace).Create(context.Background(), &transportServer, metav1.CreateOptions{}) assert.NoError(t, err) - source, err := NewF5TransportServerSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, defaultF5TransportServerNamespace, tc.annotationFilter) + source, err := NewF5TransportServerSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, defaultF5TransportServerNamespace, tc.annotationFilter, time.Duration(0)) require.NoError(t, err) assert.NotNil(t, source) diff --git a/source/f5_virtualserver.go b/source/f5_virtualserver.go index bcc4772903..0ac9966dc7 100644 --- a/source/f5_virtualserver.go +++ b/source/f5_virtualserver.go @@ -22,6 +22,7 @@ import ( "fmt" "sort" "strings" + "time" log "github.com/sirupsen/logrus" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -72,6 +73,7 @@ func NewF5VirtualServerSource( kubeClient kubernetes.Interface, namespace string, annotationFilter string, + timeout time.Duration, ) (Source, error) { informerFactory := dynamicinformer.NewFilteredDynamicSharedInformerFactory(dynamicKubeClient, 0, namespace, nil) virtualServerInformer := informerFactory.ForResource(f5VirtualServerGVR) @@ -86,7 +88,7 @@ func NewF5VirtualServerSource( informerFactory.Start(ctx.Done()) // wait for the local cache to be populated. - if err := informers.WaitForDynamicCacheSync(ctx, informerFactory); err != nil { + if err := informers.WaitForDynamicCacheSync(ctx, informerFactory, timeout); err != nil { return nil, err } diff --git a/source/f5_virtualserver_test.go b/source/f5_virtualserver_test.go index c020d4912f..5aca4a5836 100644 --- a/source/f5_virtualserver_test.go +++ b/source/f5_virtualserver_test.go @@ -20,6 +20,7 @@ import ( "context" "encoding/json" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -588,7 +589,7 @@ func TestF5VirtualServerEndpoints(t *testing.T) { _, err = fakeDynamicClient.Resource(f5VirtualServerGVR).Namespace(defaultF5VirtualServerNamespace).Create(context.Background(), &virtualServer, metav1.CreateOptions{}) assert.NoError(t, err) - source, err := NewF5VirtualServerSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, defaultF5VirtualServerNamespace, tc.annotationFilter) + source, err := NewF5VirtualServerSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, defaultF5VirtualServerNamespace, tc.annotationFilter, time.Duration(0)) require.NoError(t, err) assert.NotNil(t, source) diff --git a/source/gateway.go b/source/gateway.go index 0211bc156a..e8141521ec 100644 --- a/source/gateway.go +++ b/source/gateway.go @@ -29,6 +29,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" kubeinformers "k8s.io/client-go/informers" coreinformers "k8s.io/client-go/informers/core/v1" "k8s.io/client-go/tools/cache" @@ -195,19 +196,19 @@ func newGatewayRouteSource( nsInformer := kubeInformerFactory.Core().V1().Namespaces() // TODO: Namespace informer should be shared across gateway sources. nsInformer.Informer() // Register with factory before starting. - informerFactory.Start(ctx.Done()) - kubeInformerFactory.Start(ctx.Done()) + informerFactory.Start(wait.NeverStop) + kubeInformerFactory.Start(wait.NeverStop) if rtInformerFactory != informerFactory { - rtInformerFactory.Start(ctx.Done()) + rtInformerFactory.Start(wait.NeverStop) - if err := informers.WaitForCacheSync(ctx, rtInformerFactory); err != nil { + if err := informers.WaitForCacheSync(ctx, rtInformerFactory, config.CacheSyncTimeout); err != nil { return nil, err } } - if err := informers.WaitForCacheSync(ctx, informerFactory); err != nil { + if err := informers.WaitForCacheSync(ctx, informerFactory, config.CacheSyncTimeout); err != nil { return nil, err } - if err := informers.WaitForCacheSync(ctx, kubeInformerFactory); err != nil { + if err := informers.WaitForCacheSync(ctx, kubeInformerFactory, config.CacheSyncTimeout); err != nil { return nil, err } diff --git a/source/gloo_proxy.go b/source/gloo_proxy.go index 71022dbf73..9db43bdce3 100644 --- a/source/gloo_proxy.go +++ b/source/gloo_proxy.go @@ -22,6 +22,7 @@ import ( "fmt" "maps" "strings" + "time" log "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" @@ -145,7 +146,7 @@ type glooSource struct { // NewGlooSource creates a new glooSource with the given config func NewGlooSource(ctx context.Context, dynamicKubeClient dynamic.Interface, kubeClient kubernetes.Interface, - glooNamespaces []string) (Source, error) { + glooNamespaces []string, timeout time.Duration) (Source, error) { informerFactory := kubeinformers.NewSharedInformerFactory(kubeClient, 0) serviceInformer := informerFactory.Core().V1().Services() ingressInformer := informerFactory.Networking().V1().Ingresses() @@ -165,10 +166,10 @@ func NewGlooSource(ctx context.Context, dynamicKubeClient dynamic.Interface, kub informerFactory.Start(ctx.Done()) dynamicInformerFactory.Start(ctx.Done()) - if err := informers.WaitForCacheSync(ctx, informerFactory); err != nil { + if err := informers.WaitForCacheSync(ctx, informerFactory, timeout); err != nil { return nil, err } - if err := informers.WaitForDynamicCacheSync(ctx, dynamicInformerFactory); err != nil { + if err := informers.WaitForDynamicCacheSync(ctx, dynamicInformerFactory, timeout); err != nil { return nil, err } diff --git a/source/gloo_proxy_test.go b/source/gloo_proxy_test.go index b79e73c094..a3bfeb40ba 100644 --- a/source/gloo_proxy_test.go +++ b/source/gloo_proxy_test.go @@ -21,6 +21,7 @@ import ( "encoding/json" "fmt" "testing" + "time" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" @@ -574,7 +575,7 @@ func TestGlooSource(t *testing.T) { _, err = fakeDynamicClient.Resource(gatewayGVR).Namespace(gatewayIngressAnnotatedProxyGateway.Namespace).Create(context.Background(), &gatewayIngressAnnotatedProxyGatewayUnstructured, metav1.CreateOptions{}) assert.NoError(t, err) - source, err := NewGlooSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, []string{defaultGlooNamespace}) + source, err := NewGlooSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, []string{defaultGlooNamespace}, time.Duration(0)) assert.NoError(t, err) assert.NotNil(t, source) diff --git a/source/informers/informers.go b/source/informers/informers.go index 6bcef79296..a7eea88954 100644 --- a/source/informers/informers.go +++ b/source/informers/informers.go @@ -26,41 +26,61 @@ import ( ) const ( - defaultRequestTimeout = 60 + DefaultCacheSyncTimeout = 60 ) type informerFactory interface { WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool } -type dynamicInformerFactory interface { - WaitForCacheSync(stopCh <-chan struct{}) map[schema.GroupVersionResource]bool -} +// WaitForCacheSync waits for all informers in the factory to sync their caches. +// Returns an error if any informer fails to sync within the given timeout. +func WaitForCacheSync(ctx context.Context, factory informerFactory, timeout time.Duration) error { + if timeout <= 0 { + timeout = DefaultCacheSyncTimeout * time.Second + } + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() -func WaitForCacheSync(ctx context.Context, factory informerFactory) error { - return waitForCacheSync(ctx, factory.WaitForCacheSync) + syncResults := factory.WaitForCacheSync(ctx.Done()) + for typ, done := range syncResults { + if !done { + select { + case <-ctx.Done(): + return fmt.Errorf("cache sync for %v timed out after %s: %w", typ, timeout, ctx.Err()) + default: + return fmt.Errorf("cache sync for %v failed", typ) + } + } + } + + return nil } -func WaitForDynamicCacheSync(ctx context.Context, factory dynamicInformerFactory) error { - return waitForCacheSync(ctx, factory.WaitForCacheSync) +type dynamicInformerFactory interface { + WaitForCacheSync(stopCh <-chan struct{}) map[schema.GroupVersionResource]bool } -// waitForCacheSync waits for informer caches to sync with a default timeout. -// Returns an error if any cache fails to sync, wrapping the context error if a timeout occurred. -func waitForCacheSync[K comparable](ctx context.Context, waitFunc func(<-chan struct{}) map[K]bool) error { - // The function receives a ctx but then creates a new timeout, - // effectively overriding whatever deadline the caller may have set. - // If the caller passed a context with a 30s timeout, this function ignores it and waits 60s anyway. - timeout := defaultRequestTimeout * time.Second +// WaitForDynamicCacheSync waits for all dynamic informers in the factory to sync their caches. +// Returns an error if any informer fails to sync within the given timeout. +func WaitForDynamicCacheSync(ctx context.Context, factory dynamicInformerFactory, timeout time.Duration) error { + if timeout <= 0 { + timeout = DefaultCacheSyncTimeout * time.Second + } ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() - for typ, done := range waitFunc(ctx.Done()) { + + syncResults := factory.WaitForCacheSync(ctx.Done()) + for typ, done := range syncResults { if !done { - if ctx.Err() != nil { - return fmt.Errorf("failed to sync %v after %s: %w", typ, timeout, ctx.Err()) + select { + case <-ctx.Done(): + return fmt.Errorf("cache sync for %v timed out after %s: %w", typ, timeout, ctx.Err()) + default: + return fmt.Errorf("cache sync for %v failed", typ) } - return fmt.Errorf("failed to sync %v", typ) } } + return nil } diff --git a/source/informers/informers_test.go b/source/informers/informers_test.go index 24329b3671..571bec54c1 100644 --- a/source/informers/informers_test.go +++ b/source/informers/informers_test.go @@ -45,37 +45,27 @@ func TestWaitForCacheSync(t *testing.T) { tests := []struct { name string syncResults map[reflect.Type]bool - expectError bool - errorMsg string + wantErr bool }{ { name: "all caches synced", syncResults: map[reflect.Type]bool{reflect.TypeFor[string](): true}, + wantErr: false, }, { name: "some caches not synced", syncResults: map[reflect.Type]bool{reflect.TypeFor[string](): false}, - expectError: true, - errorMsg: "failed to sync string with timeout 1m0s", - }, - { - name: "context timeout", - syncResults: map[reflect.Type]bool{reflect.TypeFor[string](): false}, - expectError: true, - errorMsg: "failed to sync string with timeout 1m0s", + wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() - factory := &mockInformerFactory{syncResults: tt.syncResults} - err := WaitForCacheSync(ctx, factory) - - if tt.expectError { + err := WaitForCacheSync(ctx, factory, 0) + if tt.wantErr { assert.Error(t, err) - assert.Errorf(t, err, tt.errorMsg) } else { assert.NoError(t, err) } @@ -87,37 +77,27 @@ func TestWaitForDynamicCacheSync(t *testing.T) { tests := []struct { name string syncResults map[schema.GroupVersionResource]bool - expectError bool - errorMsg string + wantErr bool }{ { name: "all caches synced", syncResults: map[schema.GroupVersionResource]bool{{}: true}, + wantErr: false, }, { name: "some caches not synced", syncResults: map[schema.GroupVersionResource]bool{{}: false}, - expectError: true, - errorMsg: "failed to sync string with timeout 1m0s", - }, - { - name: "context timeout", - syncResults: map[schema.GroupVersionResource]bool{{}: false}, - expectError: true, - errorMsg: "failed to sync string with timeout 1m0s", + wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() - factory := &mockDynamicInformerFactory{syncResults: tt.syncResults} - err := WaitForDynamicCacheSync(ctx, factory) - - if tt.expectError { + err := WaitForDynamicCacheSync(ctx, factory, 0) + if tt.wantErr { assert.Error(t, err) - assert.Errorf(t, err, tt.errorMsg) } else { assert.NoError(t, err) } diff --git a/source/informers/transformers_test.go b/source/informers/transformers_test.go index b816649772..dd293e459c 100644 --- a/source/informers/transformers_test.go +++ b/source/informers/transformers_test.go @@ -133,7 +133,7 @@ func TestTransformer_Service_WithFakeClient(t *testing.T) { require.NoError(t, err) factory.Start(ctx.Done()) - err = WaitForCacheSync(ctx, factory) + err = WaitForCacheSync(ctx, factory, 0) require.NoError(t, err) got, err := serviceInformer.Lister().Services(svc.Namespace).Get(svc.Name) @@ -161,7 +161,7 @@ func TestTransformer_Service_WithFakeClient(t *testing.T) { require.NoError(t, err) factory.Start(ctx.Done()) - err = WaitForCacheSync(ctx, factory) + err = WaitForCacheSync(ctx, factory, 0) require.NoError(t, err) got, err := serviceInformer.Lister().Services(svc.Namespace).Get(svc.Name) diff --git a/source/ingress.go b/source/ingress.go index fd9a513c67..92e370e45c 100644 --- a/source/ingress.go +++ b/source/ingress.go @@ -23,6 +23,7 @@ import ( "sort" "strings" "text/template" + "time" log "github.com/sirupsen/logrus" networkv1 "k8s.io/api/networking/v1" @@ -80,7 +81,9 @@ func NewIngressSource( namespace, annotationFilter, fqdnTemplate string, combineFqdnAnnotation, ignoreHostnameAnnotation, ignoreIngressTLSSpec, ignoreIngressRulesSpec bool, labelSelector labels.Selector, - ingressClassNames []string) (Source, error) { + ingressClassNames []string, + timeout time.Duration, +) (Source, error) { tmpl, err := fqdn.ParseTemplate(fqdnTemplate) if err != nil { return nil, err @@ -112,7 +115,7 @@ func NewIngressSource( informerFactory.Start(ctx.Done()) // wait for the local cache to be populated. - if err := informers.WaitForCacheSync(ctx, informerFactory); err != nil { + if err := informers.WaitForCacheSync(ctx, informerFactory, timeout); err != nil { return nil, err } diff --git a/source/ingress_fqdn_test.go b/source/ingress_fqdn_test.go index 653f48e4da..9c959ab389 100644 --- a/source/ingress_fqdn_test.go +++ b/source/ingress_fqdn_test.go @@ -15,6 +15,7 @@ package source import ( "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -72,6 +73,7 @@ func TestIngressSourceNewNodeSourceWithFqdn(t *testing.T) { false, labels.Everything(), []string{}, + time.Duration(0), ) if tt.expectError { @@ -334,6 +336,7 @@ func TestIngressSourceFqdnTemplatingExamples(t *testing.T) { false, labels.Everything(), []string{}, + time.Duration(0), ) require.NoError(t, err) diff --git a/source/ingress_test.go b/source/ingress_test.go index 41f27959e5..838011e646 100644 --- a/source/ingress_test.go +++ b/source/ingress_test.go @@ -19,6 +19,7 @@ package source import ( "context" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -67,6 +68,7 @@ func (suite *IngressSuite) SetupTest() { false, labels.Everything(), []string{}, + time.Duration(0), ) suite.NoError(err, "should initialize ingress source") } @@ -131,6 +133,7 @@ func TestNewIngressSource(t *testing.T) { false, labels.Everything(), ti.ingressClassNames, + time.Duration(0), ) if ti.expectError { assert.Error(t, err) @@ -1428,6 +1431,7 @@ func testIngressEndpoints(t *testing.T) { ti.ignoreIngressRulesSpec, ti.ingressLabelSelector, ti.ingressClassNames, + time.Duration(0), ) // Informer cache has all of the ingresses. Retrieve and validate their endpoints. res, err := source.Endpoints(t.Context()) @@ -1712,6 +1716,7 @@ func TestIngressWithConfiguration(t *testing.T) { tt.cfg.IgnoreIngressRulesSpec, labels.Everything(), tt.cfg.IngressClassNames, + time.Duration(0), ) require.NoError(t, err) endpoints, err := src.Endpoints(t.Context()) diff --git a/source/istio_gateway.go b/source/istio_gateway.go index 1e57725135..47f50fd37d 100644 --- a/source/istio_gateway.go +++ b/source/istio_gateway.go @@ -22,6 +22,7 @@ import ( "sort" "strings" "text/template" + "time" log "github.com/sirupsen/logrus" networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" @@ -80,6 +81,7 @@ func NewIstioGatewaySource( fqdnTemplate string, combineFQDNAnnotation bool, ignoreHostnameAnnotation bool, + timeout time.Duration, ) (Source, error) { tmpl, err := fqdn.ParseTemplate(fqdnTemplate) if err != nil { @@ -113,10 +115,10 @@ func NewIstioGatewaySource( istioInformerFactory.Start(ctx.Done()) // wait for the local cache to be populated. - if err := informers.WaitForCacheSync(ctx, informerFactory); err != nil { + if err := informers.WaitForCacheSync(ctx, informerFactory, timeout); err != nil { return nil, err } - if err := informers.WaitForCacheSync(ctx, istioInformerFactory); err != nil { + if err := informers.WaitForCacheSync(ctx, istioInformerFactory, timeout); err != nil { return nil, err } @@ -168,7 +170,7 @@ func (sc *gatewaySource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, e log.Debugf("Processing gateway '%s/%s.%s' and hosts %q", gateway.Namespace, gateway.APIVersion, gateway.Name, strings.Join(gwHostnames, ",")) - gwEndpoints, err := sc.endpointsFromGateway(gwHostnames, gateway) + gwEndpoints, err := sc.endpointsFromGateway(ctx, gwHostnames, gateway) if err != nil { return nil, err } @@ -183,7 +185,7 @@ func (sc *gatewaySource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, e if err != nil { return nil, err } - return sc.endpointsFromGateway(hostnames, gateway) + return sc.endpointsFromGateway(ctx, hostnames, gateway) }, ) if err != nil { @@ -240,7 +242,7 @@ func (sc *gatewaySource) targetsFromIngress(ingressStr string, gateway *networki return targets, nil } -func (sc *gatewaySource) targetsFromGateway(gateway *networkingv1beta1.Gateway) (endpoint.Targets, error) { +func (sc *gatewaySource) targetsFromGateway(ctx context.Context, gateway *networkingv1beta1.Gateway) (endpoint.Targets, error) { targets := annotations.TargetsFromTargetAnnotation(gateway.Annotations) if len(targets) > 0 { return targets, nil @@ -255,11 +257,11 @@ func (sc *gatewaySource) targetsFromGateway(gateway *networkingv1beta1.Gateway) } // endpointsFromGatewayConfig extracts the endpoints from an Istio Gateway Config object -func (sc *gatewaySource) endpointsFromGateway(hostnames []string, gateway *networkingv1beta1.Gateway) ([]*endpoint.Endpoint, error) { +func (sc *gatewaySource) endpointsFromGateway(ctx context.Context, hostnames []string, gateway *networkingv1beta1.Gateway) ([]*endpoint.Endpoint, error) { var endpoints []*endpoint.Endpoint var err error - targets, err := sc.targetsFromGateway(gateway) + targets, err := sc.targetsFromGateway(ctx, gateway) if err != nil { return nil, err } diff --git a/source/istio_gateway_fqdn_test.go b/source/istio_gateway_fqdn_test.go deleted file mode 100644 index b4781e535f..0000000000 --- a/source/istio_gateway_fqdn_test.go +++ /dev/null @@ -1,519 +0,0 @@ -/* -Copyright 2026 The Kubernetes Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package source - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - istionetworking "istio.io/api/networking/v1beta1" - networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" - istiofake "istio.io/client-go/pkg/clientset/versioned/fake" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/intstr" - "k8s.io/client-go/kubernetes/fake" - "sigs.k8s.io/external-dns/source/annotations" - - "sigs.k8s.io/external-dns/endpoint" -) - -func TestIstioGatewaySourceNewSourceWithFqdn(t *testing.T) { - for _, tt := range []struct { - title string - annotationFilter string - fqdnTemplate string - expectError bool - }{ - { - title: "invalid template", - expectError: true, - fqdnTemplate: "{{.Name", - }, - { - title: "valid empty template", - expectError: false, - }, - { - title: "valid template", - expectError: false, - fqdnTemplate: "{{.Name}}-{{.Namespace}}.ext-dns.test.com", - }, - { - title: "valid template with multiple hosts", - expectError: false, - fqdnTemplate: "{{.Name}}-{{.Namespace}}.ext-dns.test.com, {{.Name}}-{{.Namespace}}.ext-dna.test.com", - }, - } { - t.Run(tt.title, func(t *testing.T) { - _, err := NewIstioGatewaySource( - t.Context(), - fake.NewClientset(), - istiofake.NewSimpleClientset(), - "", - tt.annotationFilter, - tt.fqdnTemplate, - false, - false, - ) - - if tt.expectError { - assert.Error(t, err) - } else { - assert.NoError(t, err) - } - }) - } -} - -func TestIstioGatewaySourceFqdnTemplatingExamples(t *testing.T) { - for _, tt := range []struct { - title string - gateways []*networkingv1beta1.Gateway - services []*v1.Service - fqdnTemplate string - combineFqdn bool - expected []*endpoint.Endpoint - }{ - { - title: "simple templating with gateway name", - fqdnTemplate: "{{.Name}}.test.com", - expected: []*endpoint.Endpoint{ - {DNSName: "example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, - {DNSName: "my-gateway.test.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, - }, - gateways: []*networkingv1beta1.Gateway{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "my-gateway", - Namespace: "istio-system", - }, - Spec: istionetworking.Gateway{ - Selector: map[string]string{"istio": "ingressgateway"}, - Servers: []*istionetworking.Server{ - {Hosts: []string{"example.org"}}, - }, - }, - }, - }, - services: []*v1.Service{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "istio-ingressgateway", - Namespace: "istio-system", - Labels: map[string]string{"istio": "ingressgateway"}, - }, - Spec: v1.ServiceSpec{ - Type: v1.ServiceTypeLoadBalancer, - Selector: map[string]string{"istio": "ingressgateway"}, - }, - Status: v1.ServiceStatus{ - LoadBalancer: v1.LoadBalancerStatus{ - Ingress: []v1.LoadBalancerIngress{{IP: "1.2.3.4"}}, - }, - }, - }, - }, - }, - { - title: "templating with fqdn combine disabled", - fqdnTemplate: "{{.Name}}.test.com", - expected: []*endpoint.Endpoint{ - {DNSName: "example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, - }, - combineFqdn: true, - gateways: []*networkingv1beta1.Gateway{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "my-gateway", - Namespace: "istio-system", - }, - Spec: istionetworking.Gateway{ - Selector: map[string]string{"istio": "ingressgateway"}, - Servers: []*istionetworking.Server{ - {Hosts: []string{"example.org"}}, - }, - }, - }, - }, - services: []*v1.Service{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "istio-ingressgateway", - Namespace: "istio-system", - Labels: map[string]string{"istio": "ingressgateway"}, - }, - Spec: v1.ServiceSpec{ - Type: v1.ServiceTypeLoadBalancer, - Selector: map[string]string{"istio": "ingressgateway"}, - }, - Status: v1.ServiceStatus{ - LoadBalancer: v1.LoadBalancerStatus{ - Ingress: []v1.LoadBalancerIngress{{IP: "1.2.3.4"}}, - }, - }, - }, - }, - }, - { - title: "templating with namespace", - fqdnTemplate: "{{.Name}}.{{.Namespace}}.cluster.local", - expected: []*endpoint.Endpoint{ - {DNSName: "api.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"5.6.7.8"}}, - {DNSName: "api-gateway.kube-system.cluster.local", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"::ffff:192.1.56.10"}}, - {DNSName: "api-gateway.production.cluster.local", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"5.6.7.8"}}, - }, - gateways: []*networkingv1beta1.Gateway{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "api-gateway", - Namespace: "production", - }, - Spec: istionetworking.Gateway{ - Selector: map[string]string{"istio": "ingressgateway"}, - Servers: []*istionetworking.Server{ - {Hosts: []string{"api.example.org"}}, - }, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Name: "api-gateway", - Namespace: "kube-system", - }, - Spec: istionetworking.Gateway{ - Selector: map[string]string{"istio": "ingressgateway-extra"}, - }, - }, - }, - services: []*v1.Service{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "istio-ingressgateway", - Namespace: "production", - Labels: map[string]string{"istio": "ingressgateway"}, - }, - Spec: v1.ServiceSpec{ - Type: v1.ServiceTypeLoadBalancer, - Selector: map[string]string{"istio": "ingressgateway"}, - }, - Status: v1.ServiceStatus{ - LoadBalancer: v1.LoadBalancerStatus{ - Ingress: []v1.LoadBalancerIngress{{IP: "5.6.7.8"}}, - }, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Name: "kube-metrics-server", - Namespace: "kube-system", - Labels: map[string]string{"istio": "ingressgateway-extra"}, - }, - Spec: v1.ServiceSpec{ - Type: v1.ServiceTypeLoadBalancer, - Selector: map[string]string{"istio": "ingressgateway-extra"}, - }, - Status: v1.ServiceStatus{ - LoadBalancer: v1.LoadBalancerStatus{ - Ingress: []v1.LoadBalancerIngress{{IP: "::ffff:192.1.56.10"}}, - }, - }, - }, - }, - }, - { - title: "templating with complex fqdn template", - fqdnTemplate: "{{.Name}}.example.com,{{.Name}}.example.org", - expected: []*endpoint.Endpoint{ - {DNSName: "multi-gateway.example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"10.0.0.1"}}, - {DNSName: "multi-gateway.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"10.0.0.1"}}, - }, - gateways: []*networkingv1beta1.Gateway{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "multi-gateway", - Namespace: "default", - }, - Spec: istionetworking.Gateway{ - Selector: map[string]string{"istio": "ingressgateway"}, - Servers: []*istionetworking.Server{}, - }, - }, - }, - services: []*v1.Service{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "istio-ingressgateway", - Namespace: "default", - Labels: map[string]string{"istio": "ingressgateway"}, - }, - Spec: v1.ServiceSpec{ - Type: v1.ServiceTypeLoadBalancer, - Selector: map[string]string{"istio": "ingressgateway"}, - }, - Status: v1.ServiceStatus{ - LoadBalancer: v1.LoadBalancerStatus{ - Ingress: []v1.LoadBalancerIngress{{IP: "10.0.0.1"}}, - }, - }, - }, - }, - }, - { - title: "combine FQDN annotation with template", - fqdnTemplate: "{{.Name}}.internal.example.com", - expected: []*endpoint.Endpoint{ - {DNSName: "app.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"172.16.0.1"}}, - {DNSName: "combined-gateway.internal.example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"172.16.0.1"}}, - }, - gateways: []*networkingv1beta1.Gateway{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "combined-gateway", - Namespace: "default", - }, - Spec: istionetworking.Gateway{ - Selector: map[string]string{"istio": "ingressgateway"}, - Servers: []*istionetworking.Server{ - {Hosts: []string{"app.example.org"}}, - }, - }, - }, - }, - services: []*v1.Service{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "istio-ingressgateway", - Namespace: "default", - Labels: map[string]string{ - "istio": "ingressgateway", - }, - }, - Spec: v1.ServiceSpec{ - Type: v1.ServiceTypeLoadBalancer, - Selector: map[string]string{"istio": "ingressgateway"}, - }, - Status: v1.ServiceStatus{ - LoadBalancer: v1.LoadBalancerStatus{ - Ingress: []v1.LoadBalancerIngress{{IP: "172.16.0.1"}}, - }, - }, - }, - }, - }, - { - title: "templating with labels", - gateways: []*networkingv1beta1.Gateway{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "labeled-gateway", - Namespace: "default", - Labels: map[string]string{ - "environment": "staging", - }, - Annotations: map[string]string{ - annotations.TargetKey: "203.0.113.1", - }, - }, - Spec: istionetworking.Gateway{ - Selector: map[string]string{"istio": "ingressgateway"}, - Servers: []*istionetworking.Server{}, - }, - }, - }, - fqdnTemplate: "{{.Name}}.{{.Labels.environment}}.example.com", - expected: []*endpoint.Endpoint{ - {DNSName: "labeled-gateway.staging.example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"203.0.113.1"}}, - }, - }, - { - title: "srv record with node port and cluster ip services without external ips", - fqdnTemplate: "{{.Name}}.example.com", - expected: []*endpoint.Endpoint{}, - gateways: []*networkingv1beta1.Gateway{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "labeled-gateway", - Namespace: "default", - }, - Spec: istionetworking.Gateway{ - Selector: map[string]string{"istio": "ingressgateway"}, - Servers: []*istionetworking.Server{}, - }, - }, - }, - services: []*v1.Service{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "test-node-port", - Namespace: "default", - Labels: map[string]string{ - "istio": "ingressgateway", - }, - }, - Spec: v1.ServiceSpec{ - Type: v1.ServiceTypeNodePort, - Selector: map[string]string{"istio": "ingressgateway"}, - ClusterIP: "10.96.41.133", - Ports: []v1.ServicePort{ - {Name: "dns", Port: 8082, TargetPort: intstr.FromInt32(8083), Protocol: v1.ProtocolUDP, NodePort: 30083}, - {Name: "dns-tcp", Port: 2525, TargetPort: intstr.FromInt32(25256), NodePort: 25565}, - }, - }, - Status: v1.ServiceStatus{ - LoadBalancer: v1.LoadBalancerStatus{}, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Name: "test-cluster-ip", - Namespace: "default", - Labels: map[string]string{"istio": "ingressgateway"}, - }, - Spec: v1.ServiceSpec{ - Type: v1.ServiceTypeClusterIP, - Selector: map[string]string{"istio": "ingressgateway"}, - ClusterIP: "10.96.41.133", - Ports: []v1.ServicePort{ - {Name: "dns", Port: 53, TargetPort: intstr.FromInt32(30053), Protocol: v1.ProtocolUDP}, - {Name: "dns-tcp", Port: 53, TargetPort: intstr.FromInt32(30054), NodePort: 25565}, - }, - }, - }, - }, - }, - { - title: "srv record with node port and cluster ip services with external ips", - fqdnTemplate: "{{.Name}}.tld.org", - expected: []*endpoint.Endpoint{ - {DNSName: "nodeport-external.tld.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"192.168.132.253"}}, - }, - gateways: []*networkingv1beta1.Gateway{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "nodeport-external", - Namespace: "default", - }, - Spec: istionetworking.Gateway{ - Selector: map[string]string{"istio": "ingressgateway"}, - }, - }, - }, - services: []*v1.Service{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "test-node-port", - Namespace: "default", - Labels: map[string]string{ - "istio": "ingressgateway", - }, - }, - Spec: v1.ServiceSpec{ - Type: v1.ServiceTypeNodePort, - Selector: map[string]string{"istio": "ingressgateway"}, - ClusterIP: "10.96.41.133", - ExternalIPs: []string{"192.168.132.253"}, - Ports: []v1.ServicePort{ - {Name: "dns", Port: 8082, TargetPort: intstr.FromInt32(8083), Protocol: v1.ProtocolUDP, NodePort: 30083}, - {Name: "dns-tcp", Port: 2525, TargetPort: intstr.FromInt32(25256), NodePort: 25565}, - }, - }, - }, - }, - }, - { - title: "with host as subdomain in reversed order", - fqdnTemplate: "{{ range $server := .Spec.Servers }}{{ range $host := $server.Hosts }}{{ $host }}.{{ $server.Port.Name }}.{{ $server.Port.Number }}.tld.com,{{ end }}{{ end }}", - expected: []*endpoint.Endpoint{ - {DNSName: "www.bookinfo", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"192.168.132.253"}}, - {DNSName: "bookinfo", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"192.168.132.253"}}, - {DNSName: "www.bookinfo.http.443.tld.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"192.168.132.253"}}, - {DNSName: "bookinfo.dns.8080.tld.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"192.168.132.253"}}, - }, - gateways: []*networkingv1beta1.Gateway{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "nodeport-external", - Namespace: "default", - }, - Spec: istionetworking.Gateway{ - Selector: map[string]string{"istio": "ingressgateway"}, - Servers: []*istionetworking.Server{ - { - Hosts: []string{"www.bookinfo"}, - Name: "main", - Port: &istionetworking.Port{Number: 443, Name: "http", Protocol: "HTTPS"}, - }, - { - Hosts: []string{"bookinfo"}, - Name: "debug", - Port: &istionetworking.Port{Number: 8080, Name: "dns", Protocol: "UDP"}, - }, - }, - }, - }, - }, - services: []*v1.Service{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "test-node-port", - Namespace: "default", - Labels: map[string]string{ - "istio": "ingressgateway", - }, - }, - Spec: v1.ServiceSpec{ - Type: v1.ServiceTypeNodePort, - Selector: map[string]string{"istio": "ingressgateway"}, - ClusterIP: "10.96.41.133", - ExternalIPs: []string{"192.168.132.253"}, - }, - }, - }, - }, - } { - t.Run(tt.title, func(t *testing.T) { - kubeClient := fake.NewClientset() - istioClient := istiofake.NewSimpleClientset() - - for _, svc := range tt.services { - _, err := kubeClient.CoreV1().Services(svc.Namespace).Create(t.Context(), svc, metav1.CreateOptions{}) - require.NoError(t, err) - } - - for _, gw := range tt.gateways { - _, err := istioClient.NetworkingV1beta1().Gateways(gw.Namespace).Create(t.Context(), gw, metav1.CreateOptions{}) - require.NoError(t, err) - } - - src, err := NewIstioGatewaySource( - t.Context(), - kubeClient, - istioClient, - "", - "", - tt.fqdnTemplate, - !tt.combineFqdn, - false, - ) - require.NoError(t, err) - - endpoints, err := src.Endpoints(t.Context()) - require.NoError(t, err) - - validateEndpoints(t, endpoints, tt.expected) - }) - } -} diff --git a/source/istio_gateway_test.go b/source/istio_gateway_test.go index fb4e29d08a..5a551fbc8c 100644 --- a/source/istio_gateway_test.go +++ b/source/istio_gateway_test.go @@ -21,6 +21,7 @@ import ( "errors" "fmt" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -104,6 +105,7 @@ func (suite *GatewaySuite) SetupTest() { "{{.Name}}", false, false, + time.Duration(0), ) suite.NoError(err, "should initialize gateway source") suite.NoError(err, "should succeed") @@ -178,6 +180,7 @@ func TestNewIstioGatewaySource(t *testing.T) { ti.fqdnTemplate, ti.combineFQDNAndAnnotation, false, + time.Duration(0), ) if ti.expectError { assert.Error(t, err) @@ -506,7 +509,7 @@ func testEndpointsFromGatewayConfig(t *testing.T) { require.NoError(t, err) } else if hostnames, err := source.hostNamesFromGateway(gatewayCfg); err != nil { require.NoError(t, err) - } else if endpoints, err := source.endpointsFromGateway(hostnames, gatewayCfg); err != nil { + } else if endpoints, err := source.endpointsFromGateway(context.Background(), hostnames, gatewayCfg); err != nil { require.NoError(t, err) } else { validateEndpoints(t, endpoints, ti.expected) @@ -1516,6 +1519,7 @@ func testGatewayEndpoints(t *testing.T) { ti.fqdnTemplate, ti.combineFQDNAndAnnotation, ti.ignoreHostnameAnnotation, + time.Duration(0), ) require.NoError(t, err) @@ -1629,6 +1633,7 @@ func TestGatewaySource_GWSelectorMatchServiceSelector(t *testing.T) { "", false, false, + time.Duration(0), ) require.NoError(t, err) require.NotNil(t, src) @@ -1713,7 +1718,8 @@ func TestTransformerInIstioGatewaySource(t *testing.T) { "", "", false, - false) + false, + time.Duration(0)) require.NoError(t, err) gwSource, ok := src.(*gatewaySource) require.True(t, ok) @@ -1871,6 +1877,7 @@ func TestSingleGatewayMultipleServicesPointingToSameLoadBalancer(t *testing.T) { "", false, false, + time.Duration(0), ) require.NoError(t, err) require.NotNil(t, src) @@ -1913,6 +1920,7 @@ func newTestGatewaySource(loadBalancerList []fakeIngressGatewayService, ingressL "{{.Name}}", false, false, + time.Duration(0), ) if err != nil { return nil, err diff --git a/source/istio_virtualservice.go b/source/istio_virtualservice.go index 372a8e09de..a7d3024faf 100644 --- a/source/istio_virtualservice.go +++ b/source/istio_virtualservice.go @@ -24,6 +24,7 @@ import ( "sort" "strings" "text/template" + "time" log "github.com/sirupsen/logrus" v1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" @@ -82,6 +83,7 @@ func NewIstioVirtualServiceSource( fqdnTemplate string, combineFQDNAnnotation bool, ignoreHostnameAnnotation bool, + timeout time.Duration, ) (Source, error) { tmpl, err := fqdn.ParseTemplate(fqdnTemplate) if err != nil { @@ -118,10 +120,10 @@ func NewIstioVirtualServiceSource( istioInformerFactory.Start(ctx.Done()) // wait for the local cache to be populated. - if err := informers.WaitForCacheSync(ctx, informerFactory); err != nil { + if err := informers.WaitForCacheSync(ctx, informerFactory, timeout); err != nil { return nil, err } - if err := informers.WaitForCacheSync(ctx, istioInformerFactory); err != nil { + if err := informers.WaitForCacheSync(ctx, istioInformerFactory, timeout); err != nil { return nil, err } diff --git a/source/istio_virtualservice_fqdn_test.go b/source/istio_virtualservice_fqdn_test.go deleted file mode 100644 index bb265795e0..0000000000 --- a/source/istio_virtualservice_fqdn_test.go +++ /dev/null @@ -1,670 +0,0 @@ -/* -Copyright 2025 The Kubernetes Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package source - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - istionetworking "istio.io/api/networking/v1beta1" - networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" - istiofake "istio.io/client-go/pkg/clientset/versioned/fake" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes/fake" - "sigs.k8s.io/external-dns/source/annotations" - - "sigs.k8s.io/external-dns/endpoint" -) - -func TestIstioVirtualServiceSourceNewSourceWithFqdn(t *testing.T) { - for _, tt := range []struct { - title string - annotationFilter string - fqdnTemplate string - expectError bool - }{ - { - title: "invalid template", - expectError: true, - fqdnTemplate: "{{.Name", - }, - { - title: "valid empty template", - expectError: false, - }, - { - title: "valid template", - expectError: false, - fqdnTemplate: "{{.Name}}-{{.Namespace}}.ext-dns.test.com", - }, - { - title: "valid template with multiple hosts", - expectError: false, - fqdnTemplate: "{{.Name}}-{{.Namespace}}.ext-dns.test.com, {{.Name}}-{{.Namespace}}.ext-dna.test.com", - }, - } { - t.Run(tt.title, func(t *testing.T) { - _, err := NewIstioVirtualServiceSource( - t.Context(), - fake.NewClientset(), - istiofake.NewSimpleClientset(), - "", - tt.annotationFilter, - tt.fqdnTemplate, - false, - false, - ) - - if tt.expectError { - assert.Error(t, err) - } else { - assert.NoError(t, err) - } - }) - } -} - -func TestIstioVirtualServiceSourceFqdnTemplatingExamples(t *testing.T) { - annotations.SetAnnotationPrefix("external-dns.alpha.kubernetes.io/") - for _, tt := range []struct { - title string - virtualServices []*networkingv1beta1.VirtualService - gateways []*networkingv1beta1.Gateway - services []*v1.Service - fqdnTemplate string - combineFqdn bool - expected []*endpoint.Endpoint - }{ - { - title: "simple templating with virtualservice name", - fqdnTemplate: "{{.Name}}.test.com", - expected: []*endpoint.Endpoint{ - {DNSName: "app.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, - {DNSName: "my-virtualservice.test.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, - }, - virtualServices: []*networkingv1beta1.VirtualService{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "my-virtualservice", - Namespace: "default", - }, - Spec: istionetworking.VirtualService{ - Hosts: []string{"app.example.org"}, - Gateways: []string{"my-gateway"}, - }, - }, - }, - gateways: []*networkingv1beta1.Gateway{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "my-gateway", - Namespace: "default", - }, - Spec: istionetworking.Gateway{ - Selector: map[string]string{"istio": "ingressgateway"}, - Servers: []*istionetworking.Server{ - {Hosts: []string{"*"}}, - }, - }, - }, - }, - services: []*v1.Service{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "istio-ingressgateway", - Namespace: "default", - Labels: map[string]string{"istio": "ingressgateway"}, - }, - Spec: v1.ServiceSpec{ - Type: v1.ServiceTypeLoadBalancer, - Selector: map[string]string{"istio": "ingressgateway"}, - }, - Status: v1.ServiceStatus{ - LoadBalancer: v1.LoadBalancerStatus{ - Ingress: []v1.LoadBalancerIngress{{IP: "1.2.3.4"}}, - }, - }, - }, - }, - }, - { - title: "templating with fqdn combine disabled", - fqdnTemplate: "{{.Name}}.test.com", - expected: []*endpoint.Endpoint{ - {DNSName: "app.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, - }, - combineFqdn: true, - virtualServices: []*networkingv1beta1.VirtualService{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "my-virtualservice", - Namespace: "default", - }, - Spec: istionetworking.VirtualService{ - Hosts: []string{"app.example.org"}, - Gateways: []string{"my-gateway"}, - }, - }, - }, - gateways: []*networkingv1beta1.Gateway{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "my-gateway", - Namespace: "default", - }, - Spec: istionetworking.Gateway{ - Selector: map[string]string{"istio": "ingressgateway"}, - Servers: []*istionetworking.Server{ - {Hosts: []string{"*"}}, - }, - }, - }, - }, - services: []*v1.Service{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "istio-ingressgateway", - Namespace: "default", - Labels: map[string]string{"istio": "ingressgateway"}, - }, - Spec: v1.ServiceSpec{ - Type: v1.ServiceTypeLoadBalancer, - Selector: map[string]string{"istio": "ingressgateway"}, - }, - Status: v1.ServiceStatus{ - LoadBalancer: v1.LoadBalancerStatus{ - Ingress: []v1.LoadBalancerIngress{{IP: "1.2.3.4"}}, - }, - }, - }, - }, - }, - { - title: "templating with namespace", - fqdnTemplate: "{{.Name}}.{{.Namespace}}.cluster.local", - expected: []*endpoint.Endpoint{ - {DNSName: "api.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"5.6.7.8"}}, - {DNSName: "api-service.production.cluster.local", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"5.6.7.8"}}, - {DNSName: "web.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"::ffff:192.1.56.10"}}, - {DNSName: "web-service.staging.cluster.local", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"::ffff:192.1.56.10"}}, - }, - virtualServices: []*networkingv1beta1.VirtualService{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "api-service", - Namespace: "production", - }, - Spec: istionetworking.VirtualService{ - Hosts: []string{"api.example.org"}, - Gateways: []string{"api-gateway"}, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Name: "web-service", - Namespace: "staging", - }, - Spec: istionetworking.VirtualService{ - Hosts: []string{"web.example.org"}, - Gateways: []string{"web-gateway"}, - }, - }, - }, - gateways: []*networkingv1beta1.Gateway{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "api-gateway", - Namespace: "production", - }, - Spec: istionetworking.Gateway{ - Selector: map[string]string{"istio": "ingressgateway"}, - Servers: []*istionetworking.Server{ - {Hosts: []string{"*"}}, - }, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Name: "web-gateway", - Namespace: "staging", - }, - Spec: istionetworking.Gateway{ - Selector: map[string]string{"istio": "ingressgateway-staging"}, - Servers: []*istionetworking.Server{ - {Hosts: []string{"*"}}, - }, - }, - }, - }, - services: []*v1.Service{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "istio-ingressgateway", - Namespace: "production", - Labels: map[string]string{"istio": "ingressgateway"}, - }, - Spec: v1.ServiceSpec{ - Type: v1.ServiceTypeLoadBalancer, - Selector: map[string]string{"istio": "ingressgateway"}, - }, - Status: v1.ServiceStatus{ - LoadBalancer: v1.LoadBalancerStatus{ - Ingress: []v1.LoadBalancerIngress{{IP: "5.6.7.8"}}, - }, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Name: "istio-ingressgateway-staging", - Namespace: "staging", - Labels: map[string]string{"istio": "ingressgateway-staging"}, - }, - Spec: v1.ServiceSpec{ - Type: v1.ServiceTypeLoadBalancer, - Selector: map[string]string{"istio": "ingressgateway-staging"}, - }, - Status: v1.ServiceStatus{ - LoadBalancer: v1.LoadBalancerStatus{ - Ingress: []v1.LoadBalancerIngress{{IP: "::ffff:192.1.56.10"}}, - }, - }, - }, - }, - }, - { - title: "templating with multiple fqdn templates", - fqdnTemplate: "{{.Name}}.example.com,{{.Name}}.example.org", - expected: []*endpoint.Endpoint{ - {DNSName: "multi-host.example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"10.0.0.1"}}, - {DNSName: "multi-host.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"10.0.0.1"}}, - }, - virtualServices: []*networkingv1beta1.VirtualService{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "multi-host", - Namespace: "default", - }, - Spec: istionetworking.VirtualService{ - Gateways: []string{"my-gateway"}, - }, - }, - }, - gateways: []*networkingv1beta1.Gateway{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "my-gateway", - Namespace: "default", - }, - Spec: istionetworking.Gateway{ - Selector: map[string]string{"istio": "ingressgateway"}, - Servers: []*istionetworking.Server{ - {Hosts: []string{"*"}}, - }, - }, - }, - }, - services: []*v1.Service{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "istio-ingressgateway", - Namespace: "default", - Labels: map[string]string{"istio": "ingressgateway"}, - }, - Spec: v1.ServiceSpec{ - Type: v1.ServiceTypeLoadBalancer, - Selector: map[string]string{"istio": "ingressgateway"}, - }, - Status: v1.ServiceStatus{ - LoadBalancer: v1.LoadBalancerStatus{ - Ingress: []v1.LoadBalancerIngress{{IP: "10.0.0.1"}}, - }, - }, - }, - }, - }, - { - title: "combine FQDN annotation with template", - fqdnTemplate: "{{.Name}}.internal.example.com", - expected: []*endpoint.Endpoint{ - {DNSName: "app.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"172.16.0.1"}}, - {DNSName: "combined-vs.internal.example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"172.16.0.1"}}, - }, - virtualServices: []*networkingv1beta1.VirtualService{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "combined-vs", - Namespace: "default", - }, - Spec: istionetworking.VirtualService{ - Hosts: []string{"app.example.org"}, - Gateways: []string{"my-gateway"}, - }, - }, - }, - gateways: []*networkingv1beta1.Gateway{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "my-gateway", - Namespace: "default", - }, - Spec: istionetworking.Gateway{ - Selector: map[string]string{"istio": "ingressgateway"}, - Servers: []*istionetworking.Server{ - {Hosts: []string{"*"}}, - }, - }, - }, - }, - services: []*v1.Service{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "istio-ingressgateway", - Namespace: "default", - Labels: map[string]string{"istio": "ingressgateway"}, - }, - Spec: v1.ServiceSpec{ - Type: v1.ServiceTypeLoadBalancer, - Selector: map[string]string{"istio": "ingressgateway"}, - }, - Status: v1.ServiceStatus{ - LoadBalancer: v1.LoadBalancerStatus{ - Ingress: []v1.LoadBalancerIngress{{IP: "172.16.0.1"}}, - }, - }, - }, - }, - }, - { - title: "complex templating with labels and hosts", - fqdnTemplate: "{{ if .Labels.env }}{{.Name}}.{{.Labels.env}}.ex{{ end }}", - expected: []*endpoint.Endpoint{ - {DNSName: "labeled-vs.dev.ex", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"172.16.0.1"}}, - }, - virtualServices: []*networkingv1beta1.VirtualService{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "labeled-vs", - Namespace: "default", - Labels: map[string]string{ - "env": "dev", - }, - }, - Spec: istionetworking.VirtualService{ - Gateways: []string{"my-gateway"}, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Name: "no-labels", - Namespace: "default", - }, - Spec: istionetworking.VirtualService{ - Gateways: []string{"my-gateway"}, - }, - }, - }, - gateways: []*networkingv1beta1.Gateway{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "my-gateway", - Namespace: "default", - }, - Spec: istionetworking.Gateway{ - Selector: map[string]string{"istio": "ingressgateway"}, - Servers: []*istionetworking.Server{ - {Hosts: []string{"*"}}, - }, - }, - }, - }, - services: []*v1.Service{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "istio-ingressgateway", - Namespace: "default", - }, - Spec: v1.ServiceSpec{ - Type: v1.ServiceTypeLoadBalancer, - Selector: map[string]string{"istio": "ingressgateway"}, - }, - Status: v1.ServiceStatus{ - LoadBalancer: v1.LoadBalancerStatus{ - Ingress: []v1.LoadBalancerIngress{{IP: "172.16.0.1"}}, - }, - }, - }, - }, - }, - { - title: "templating with cross-namespace gateway reference", - fqdnTemplate: "{{.Name}}.{{.Namespace}}.svc.cluster.local", - expected: []*endpoint.Endpoint{ - {DNSName: "cross-ns.example.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"lb.example.com"}}, - {DNSName: "cross-ns-vs.app-namespace.svc.cluster.local", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"lb.example.com"}}, - }, - virtualServices: []*networkingv1beta1.VirtualService{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "cross-ns-vs", - Namespace: "app-namespace", - }, - Spec: istionetworking.VirtualService{ - Hosts: []string{"cross-ns.example.org"}, - Gateways: []string{"istio-system/shared-gateway"}, - }, - }, - }, - gateways: []*networkingv1beta1.Gateway{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "shared-gateway", - Namespace: "istio-system", - }, - Spec: istionetworking.Gateway{ - Selector: map[string]string{"istio": "ingressgateway"}, - Servers: []*istionetworking.Server{ - {Hosts: []string{"*"}}, - }, - }, - }, - }, - services: []*v1.Service{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "istio-ingressgateway", - Namespace: "istio-system", - Labels: map[string]string{"istio": "ingressgateway"}, - }, - Spec: v1.ServiceSpec{ - Type: v1.ServiceTypeLoadBalancer, - Selector: map[string]string{"istio": "ingressgateway"}, - }, - Status: v1.ServiceStatus{ - LoadBalancer: v1.LoadBalancerStatus{ - Ingress: []v1.LoadBalancerIngress{{Hostname: "lb.example.com"}}, - }, - }, - }, - }, - }, - { - title: "virtualservice with multiple hosts in spec", - fqdnTemplate: "{{.Name}}.internal.local", - expected: []*endpoint.Endpoint{ - {DNSName: "app1.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"192.168.1.100"}}, - {DNSName: "app2.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"192.168.1.100"}}, - {DNSName: "app3.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"192.168.1.100"}}, - {DNSName: "multi-host-vs.internal.local", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"192.168.1.100"}}, - }, - virtualServices: []*networkingv1beta1.VirtualService{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "multi-host-vs", - Namespace: "default", - }, - Spec: istionetworking.VirtualService{ - Hosts: []string{"app1.example.org", "app2.example.org", "app3.example.org"}, - Gateways: []string{"my-gateway"}, - }, - }, - }, - gateways: []*networkingv1beta1.Gateway{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "my-gateway", - Namespace: "default", - }, - Spec: istionetworking.Gateway{ - Selector: map[string]string{"istio": "ingressgateway"}, - Servers: []*istionetworking.Server{ - {Hosts: []string{"*"}}, - }, - }, - }, - }, - services: []*v1.Service{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "istio-ingressgateway", - Namespace: "default", - Labels: map[string]string{"istio": "ingressgateway"}, - }, - Spec: v1.ServiceSpec{ - Type: v1.ServiceTypeLoadBalancer, - Selector: map[string]string{"istio": "ingressgateway"}, - }, - Status: v1.ServiceStatus{ - LoadBalancer: v1.LoadBalancerStatus{ - Ingress: []v1.LoadBalancerIngress{{IP: "192.168.1.100"}}, - }, - }, - }, - }, - }, - { - title: "virtualservice with no matching gateway (no endpoints from spec)", - fqdnTemplate: "{{.Name}}.fallback.local", - expected: []*endpoint.Endpoint{ - {DNSName: "orphan.example.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"fallback.local"}}, - }, - virtualServices: []*networkingv1beta1.VirtualService{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "orphan-vs", - Namespace: "default", - Annotations: map[string]string{ - annotations.TargetKey: "fallback.local", - }, - }, - Spec: istionetworking.VirtualService{ - Hosts: []string{"orphan.example.org"}, - Gateways: []string{"non-existent-gateway"}, - }, - }, - }, - }, - { - title: "templating with annotations expansion", - fqdnTemplate: `{{ index .ObjectMeta.Annotations "dns.company.com/subdomain" }}.company.local`, - expected: []*endpoint.Endpoint{ - {DNSName: "api-v2.company.local", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"192.168.1.100"}}, - }, - virtualServices: []*networkingv1beta1.VirtualService{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "annotated-vs", - Namespace: "default", - Annotations: map[string]string{ - "dns.company.com/subdomain": "api-v2", - annotations.TargetKey: "10.20.30.40", - }, - }, - Spec: istionetworking.VirtualService{ - Gateways: []string{"my-gateway"}, - }, - }, - }, - gateways: []*networkingv1beta1.Gateway{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "my-gateway", - Namespace: "default", - }, - Spec: istionetworking.Gateway{ - Selector: map[string]string{"istio": "ingressgateway"}, - Servers: []*istionetworking.Server{ - {Hosts: []string{"*"}}, - }, - }, - }, - }, - services: []*v1.Service{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "istio", - Namespace: "default", - }, - Spec: v1.ServiceSpec{ - Type: v1.ServiceTypeLoadBalancer, - Selector: map[string]string{"istio": "ingressgateway"}, - }, - Status: v1.ServiceStatus{ - LoadBalancer: v1.LoadBalancerStatus{ - Ingress: []v1.LoadBalancerIngress{{IP: "192.168.1.100"}}, - }, - }, - }, - }, - }, - } { - t.Run(tt.title, func(t *testing.T) { - kubeClient := fake.NewClientset() - istioClient := istiofake.NewSimpleClientset() - - for _, svc := range tt.services { - _, err := kubeClient.CoreV1().Services(svc.Namespace).Create(t.Context(), svc, metav1.CreateOptions{}) - require.NoError(t, err) - } - - for _, gw := range tt.gateways { - _, err := istioClient.NetworkingV1beta1().Gateways(gw.Namespace).Create(t.Context(), gw, metav1.CreateOptions{}) - require.NoError(t, err) - } - - for _, vs := range tt.virtualServices { - _, err := istioClient.NetworkingV1beta1().VirtualServices(vs.Namespace).Create(t.Context(), vs, metav1.CreateOptions{}) - require.NoError(t, err) - } - - src, err := NewIstioVirtualServiceSource( - t.Context(), - kubeClient, - istioClient, - "", - "", - tt.fqdnTemplate, - !tt.combineFqdn, - false, - ) - require.NoError(t, err) - - endpoints, err := src.Endpoints(t.Context()) - require.NoError(t, err) - - validateEndpoints(t, endpoints, tt.expected) - }) - } -} diff --git a/source/istio_virtualservice_test.go b/source/istio_virtualservice_test.go index f7efed5647..6c6d6c3173 100644 --- a/source/istio_virtualservice_test.go +++ b/source/istio_virtualservice_test.go @@ -21,6 +21,7 @@ import ( "errors" "fmt" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -123,6 +124,7 @@ func (suite *VirtualServiceSuite) SetupTest() { "{{.Name}}", false, false, + time.Duration(0), ) suite.NoError(err, "should initialize virtualservice source") } @@ -200,6 +202,7 @@ func TestNewIstioVirtualServiceSource(t *testing.T) { ti.fqdnTemplate, ti.combineFQDNAndAnnotation, false, + time.Duration(0), ) if ti.expectError { assert.Error(t, err) @@ -1997,6 +2000,7 @@ func testVirtualServiceEndpoints(t *testing.T) { ti.fqdnTemplate, ti.combineFQDNAndAnnotation, ti.ignoreHostnameAnnotation, + time.Duration(0), ) require.NoError(t, err) @@ -2083,6 +2087,7 @@ func newTestVirtualServiceSource(loadBalancerList []fakeIngressGatewayService, i "{{.Name}}", false, false, + time.Duration(0), ) if err != nil { return nil, err @@ -2326,6 +2331,7 @@ func TestIstioVirtualServiceSource_GWServiceSelectorMatchServiceSelector(t *test "", false, false, + time.Duration(0), ) require.NoError(t, err) require.NotNil(t, src) @@ -2410,7 +2416,8 @@ func TestTransformerInIstioGatewayVirtualServiceSource(t *testing.T) { "", "", false, - false) + false, + time.Duration(0)) require.NoError(t, err) gwSource, ok := src.(*virtualServiceSource) require.True(t, ok) diff --git a/source/kong_tcpingress.go b/source/kong_tcpingress.go index fad22fc271..8dc397b929 100644 --- a/source/kong_tcpingress.go +++ b/source/kong_tcpingress.go @@ -21,6 +21,7 @@ import ( "errors" "fmt" "sort" + "time" log "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" @@ -71,6 +72,7 @@ func NewKongTCPIngressSource( ctx context.Context, dynamicKubeClient dynamic.Interface, kubeClient kubernetes.Interface, namespace, annotationFilter string, ignoreHostnameAnnotation bool, + timeout time.Duration, ) (Source, error) { // Use shared informer to listen for add/update/delete of Host in the specified namespace. // Set resync period to 0, to prevent processing when nothing has changed. @@ -88,7 +90,7 @@ func NewKongTCPIngressSource( informerFactory.Start(ctx.Done()) // wait for the local cache to be populated. - if err := informers.WaitForDynamicCacheSync(ctx, informerFactory); err != nil { + if err := informers.WaitForDynamicCacheSync(ctx, informerFactory, timeout); err != nil { return nil, err } diff --git a/source/kong_tcpingress_test.go b/source/kong_tcpingress_test.go index 8325671bb0..476b30aca3 100644 --- a/source/kong_tcpingress_test.go +++ b/source/kong_tcpingress_test.go @@ -20,6 +20,7 @@ import ( "context" "encoding/json" "testing" + "time" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" @@ -363,7 +364,7 @@ func TestKongTCPIngressEndpoints(t *testing.T) { _, err = fakeDynamicClient.Resource(kongGroupdVersionResource).Namespace(defaultKongNamespace).Create(context.Background(), &tcpi, metav1.CreateOptions{}) assert.NoError(t, err) - source, err := NewKongTCPIngressSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, defaultKongNamespace, "kubernetes.io/ingress.class=kong", ti.ignoreHostnameAnnotation) + source, err := NewKongTCPIngressSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, defaultKongNamespace, "kubernetes.io/ingress.class=kong", ti.ignoreHostnameAnnotation, time.Duration(0)) assert.NoError(t, err) assert.NotNil(t, source) diff --git a/source/node.go b/source/node.go index 342b306471..368a42d5e9 100644 --- a/source/node.go +++ b/source/node.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "text/template" + "time" log "github.com/sirupsen/logrus" v1 "k8s.io/api/core/v1" @@ -63,7 +64,9 @@ func NewNodeSource( labelSelector labels.Selector, exposeInternalIPv6, excludeUnschedulable bool, - combineFQDNAnnotation bool) (Source, error) { + combineFQDNAnnotation bool, + timeout time.Duration, +) (Source, error) { tmpl, err := fqdn.ParseTemplate(fqdnTemplate) if err != nil { return nil, err @@ -80,7 +83,7 @@ func NewNodeSource( informerFactory.Start(ctx.Done()) // wait for the local cache to be populated. - if err := informers.WaitForCacheSync(ctx, informerFactory); err != nil { + if err := informers.WaitForCacheSync(ctx, informerFactory, timeout); err != nil { return nil, err } diff --git a/source/node_fqdn_test.go b/source/node_fqdn_test.go index f8c4bc302c..af28ca89ff 100644 --- a/source/node_fqdn_test.go +++ b/source/node_fqdn_test.go @@ -18,6 +18,7 @@ package source import ( "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -65,6 +66,7 @@ func TestNodeSourceNewNodeSourceWithFqdn(t *testing.T) { true, true, false, + time.Duration(0), ) if tt.expectError { assert.Error(t, err) @@ -321,37 +323,6 @@ func TestNodeSourceFqdnTemplatingExamples(t *testing.T) { {DNSName: "node-name-2", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"243.186.136.178"}}, }, }, - { - title: "templating with kind-based FQDNs", - fqdnTemplate: `{{ if eq .Kind "Pod" }}{{.Name}}.pod.tld{{ end }} - {{ if eq .Kind "Node" }}{{.Name}}.{{.Status.NodeInfo.Architecture}}.node.tld{{ end }}`, - expected: []*endpoint.Endpoint{ - {DNSName: "node-name-1.arm64.node.tld", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"10.0.0.1"}}, - {DNSName: "node-name-2.x86_64.node.tld", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"10.0.0.2"}}, - }, - combineFQDN: false, - nodes: []*v1.Node{ - { - ObjectMeta: metav1.ObjectMeta{Name: "node-name-1"}, - Status: v1.NodeStatus{ - Addresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "10.0.0.1"}}, - NodeInfo: v1.NodeSystemInfo{ - Architecture: "arm64", - }, - }, - Spec: v1.NodeSpec{}, - }, - { - ObjectMeta: metav1.ObjectMeta{Name: "node-name-2"}, - Status: v1.NodeStatus{ - Addresses: []v1.NodeAddress{{Type: v1.NodeExternalIP, Address: "10.0.0.2"}}, - NodeInfo: v1.NodeSystemInfo{ - Architecture: "x86_64", - }, - }, - }, - }, - }, } { t.Run(tt.title, func(t *testing.T) { kubeClient := fake.NewClientset() @@ -370,6 +341,7 @@ func TestNodeSourceFqdnTemplatingExamples(t *testing.T) { true, true, tt.combineFQDN, + time.Duration(0), ) require.NoError(t, err) diff --git a/source/node_test.go b/source/node_test.go index 353d387519..0f7acd5543 100644 --- a/source/node_test.go +++ b/source/node_test.go @@ -99,6 +99,7 @@ func testNodeSourceNewNodeSource(t *testing.T) { true, true, false, + time.Duration(0), ) if ti.expectError { @@ -445,6 +446,7 @@ func testNodeSourceEndpoints(t *testing.T) { tc.exposeInternalIPv6, tc.excludeUnschedulable, false, + time.Duration(0), ) require.NoError(t, err) @@ -558,6 +560,7 @@ func testNodeEndpointsWithIPv6(t *testing.T) { tc.exposeInternalIPv6, tc.excludeUnschedulable, false, + time.Duration(0), ) require.NoError(t, err) @@ -602,6 +605,7 @@ func TestResourceLabelIsSetForEachNodeEndpoint(t *testing.T) { false, true, false, + time.Duration(0), ) require.NoError(t, err) diff --git a/source/openshift_route.go b/source/openshift_route.go index 4721e13fde..943179ad1d 100644 --- a/source/openshift_route.go +++ b/source/openshift_route.go @@ -74,6 +74,7 @@ func NewOcpRouteSource( ignoreHostnameAnnotation bool, labelSelector labels.Selector, ocpRouterName string, + timeout time.Duration, ) (Source, error) { tmpl, err := fqdn.ParseTemplate(fqdnTemplate) if err != nil { @@ -96,7 +97,7 @@ func NewOcpRouteSource( informerFactory.Start(ctx.Done()) // wait for the local cache to be populated. - if err := informers.WaitForCacheSync(ctx, informerFactory); err != nil { + if err := informers.WaitForCacheSync(ctx, informerFactory, timeout); err != nil { return nil, err } diff --git a/source/openshift_route_fqdn_test.go b/source/openshift_route_fqdn_test.go deleted file mode 100644 index 08c02462d0..0000000000 --- a/source/openshift_route_fqdn_test.go +++ /dev/null @@ -1,357 +0,0 @@ -/* -Copyright 2026 The Kubernetes Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package source - -import ( - "context" - "testing" - - routev1 "github.com/openshift/api/route/v1" - "github.com/openshift/client-go/route/clientset/versioned/fake" - "github.com/stretchr/testify/require" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/util/intstr" - "sigs.k8s.io/external-dns/endpoint" - "sigs.k8s.io/external-dns/source/annotations" -) - -func TestOpenShiftFqdnTemplatingExamples(t *testing.T) { - for _, tt := range []struct { - title string - ocpRoute []*routev1.Route - fqdnTemplate string - combineFqdn bool - expected []*endpoint.Endpoint - }{ - { - title: "simple templating", - fqdnTemplate: "{{.Name}}.tld.com", - expected: []*endpoint.Endpoint{ - {DNSName: "example.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"router-default.example.com"}}, - {DNSName: "my-gateway.tld.com", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"router-default.example.com"}}, - }, - ocpRoute: []*routev1.Route{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "my-gateway", - Namespace: "default", - }, - Spec: routev1.RouteSpec{ - Host: "example.org", - To: routev1.RouteTargetReference{ - Kind: "Service", - Name: "my-service", - }, - TLS: &routev1.TLSConfig{}, - }, - Status: routev1.RouteStatus{ - Ingress: []routev1.RouteIngress{ - { - Host: "example.org", - RouterCanonicalHostname: "router-default.example.com", - Conditions: []routev1.RouteIngressCondition{ - { - Type: routev1.RouteAdmitted, - Status: corev1.ConditionTrue, - }, - }, - }, - }, - }, - }, - }, - }, - { - title: "templating with fqdn combine disabled", - fqdnTemplate: "{{.Name}}.tld.com", - expected: []*endpoint.Endpoint{ - {DNSName: "example.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"router-default.example.com"}}, - }, - combineFqdn: true, - ocpRoute: []*routev1.Route{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "my-gateway", - Namespace: "default", - }, - Spec: routev1.RouteSpec{}, - Status: routev1.RouteStatus{ - Ingress: []routev1.RouteIngress{ - { - Host: "example.org", - RouterCanonicalHostname: "router-default.example.com", - Conditions: []routev1.RouteIngressCondition{ - { - Type: routev1.RouteAdmitted, - Status: corev1.ConditionTrue, - }, - }, - }, - }, - }, - }, - }, - }, - { - title: "templating with namespace", - fqdnTemplate: "{{.Name}}.{{.Namespace}}.tld.com", - expected: []*endpoint.Endpoint{ - {DNSName: "my-gateway.kube-system.tld.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"10.1.1.0"}}, - }, - combineFqdn: true, - ocpRoute: []*routev1.Route{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "my-gateway", - Namespace: "kube-system", - Annotations: map[string]string{ - annotations.TargetKey: "10.1.1.0", - }, - }, - }, - }, - }, - { - title: "templating with complex fqdn template", - fqdnTemplate: "{{ .Name }}.{{ .Namespace }}.tld.com,{{ if .Labels.env }}{{ .Labels.env }}.private{{ end }}", - expected: []*endpoint.Endpoint{ - {DNSName: "no-labels-route-3.default.tld.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"10.1.1.3"}}, - {DNSName: "route-2.default.tld.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"10.1.1.3"}}, - {DNSName: "dev.private", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"10.1.1.3"}}, - {DNSName: "route-1.kube-system.tld.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"10.1.1.0"}}, - {DNSName: "prod.private", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"10.1.1.0"}}, - }, - combineFqdn: true, - ocpRoute: []*routev1.Route{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "route-1", - Namespace: "kube-system", - Labels: map[string]string{ - "env": "prod", - }, - Annotations: map[string]string{ - "env": "prod", - annotations.TargetKey: "10.1.1.0", - }, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Name: "route-2", - Namespace: "default", - Labels: map[string]string{ - "env": "dev", - }, - Annotations: map[string]string{ - annotations.TargetKey: "10.1.1.3", - }, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Name: "no-labels-route-3", - Namespace: "default", - Annotations: map[string]string{ - annotations.TargetKey: "10.1.1.3", - }, - }, - }, - }, - }, - { - title: "template that skips when field is missing", - fqdnTemplate: "{{ if and .Spec.Port .Spec.Port.TargetPort }}{{ .Name }}.{{ .Spec.Port.TargetPort }}.tld.com{{ end }}", - expected: []*endpoint.Endpoint{ - {DNSName: "route-1.80.tld.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"10.1.1.0"}}, - }, - ocpRoute: []*routev1.Route{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "route-1", - Namespace: "kube-system", - Annotations: map[string]string{ - annotations.TargetKey: "10.1.1.0", - }, - }, - Spec: routev1.RouteSpec{ - Port: &routev1.RoutePort{ - TargetPort: intstr.FromString("80"), - }, - }, - Status: routev1.RouteStatus{}, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Name: "route-2", - Namespace: "default", - Annotations: map[string]string{ - annotations.TargetKey: "10.1.1.3", - }, - }, - }, - }, - }, - { - title: "get canonical hostnames for admitted routes", - fqdnTemplate: "{{ $name := .Name }}{{ range $ingress := .Status.Ingress }}{{ range $ingress.Conditions }}{{ if and (eq .Type \"Admitted\") (eq .Status \"True\") }}{{ $ingress.Host }},{{ end }}{{ end }}{{ end }}", - expected: []*endpoint.Endpoint{ - {DNSName: "cluster.example.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"router-dmz.apps.dmz.example.com"}}, - {DNSName: "cluster.example.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"router-dmz.apps.dmz.example.com"}}, - {DNSName: "apps.example.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"router-dmz.apps.dmz.example.com"}}, - }, - ocpRoute: []*routev1.Route{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "my-route", - Namespace: "kube-system", - Annotations: map[string]string{}, - }, - Status: routev1.RouteStatus{ - Ingress: []routev1.RouteIngress{ - { - Host: "cluster.example.org", - RouterCanonicalHostname: "router-dmz.apps.dmz.example.com", - Conditions: []routev1.RouteIngressCondition{ - { - Type: routev1.RouteAdmitted, - Status: corev1.ConditionTrue, - }, - }, - }, - { - Host: "apps.example.org", - RouterCanonicalHostname: "router-internal.apps.internal.example.com", - Conditions: []routev1.RouteIngressCondition{ - { - Type: routev1.RouteAdmitted, - Status: corev1.ConditionTrue, - }, - }, - }, - { - Host: "wrong.example.org", - RouterCanonicalHostname: "router-default.apps.cluster.example.com", - Conditions: []routev1.RouteIngressCondition{ - { - Type: routev1.RouteAdmitted, - Status: corev1.ConditionFalse, - }, - }, - }, - }, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Name: "route-2", - Namespace: "default", - Annotations: map[string]string{ - annotations.TargetKey: "10.1.1.3", - }, - }, - }, - }, - }, - { - title: "get canonical hostnames for admitted routes without prefix", - fqdnTemplate: "{{ $name := .Name }}{{ range $ingress := .Status.Ingress }}{{ range $ingress.Conditions }}{{ if and (eq .Type \"Admitted\") (eq .Status \"True\") }}{{ with $ingress.RouterCanonicalHostname }}{{ $name }}.{{ trimPrefix . \"router-\" }},{{ end }}{{ end }}{{ end }}{{ end }}", - expected: []*endpoint.Endpoint{ - {DNSName: "cluster.example.org", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"router-dmz.apps.dmz.example.com"}}, - {DNSName: "my-route.dmz.apps.dmz.example.com", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"router-dmz.apps.dmz.example.com"}}, - {DNSName: "my-route.internal.apps.internal.example.com", RecordType: endpoint.RecordTypeCNAME, Targets: endpoint.Targets{"router-dmz.apps.dmz.example.com"}}, - }, - ocpRoute: []*routev1.Route{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "my-route", - Namespace: "kube-system", - Annotations: map[string]string{}, - }, - Status: routev1.RouteStatus{ - Ingress: []routev1.RouteIngress{ - { - Host: "cluster.example.org", - RouterCanonicalHostname: "router-dmz.apps.dmz.example.com", - Conditions: []routev1.RouteIngressCondition{ - { - Type: routev1.RouteAdmitted, - Status: corev1.ConditionTrue, - }, - }, - }, - { - Host: "apps.example.org", - RouterCanonicalHostname: "router-internal.apps.internal.example.com", - Conditions: []routev1.RouteIngressCondition{ - { - Type: routev1.RouteAdmitted, - Status: corev1.ConditionTrue, - }, - }, - }, - { - Host: "wrong.example.org", - RouterCanonicalHostname: "router-default.apps.cluster.example.com", - Conditions: []routev1.RouteIngressCondition{ - { - Type: routev1.RouteAdmitted, - Status: corev1.ConditionFalse, - }, - }, - }, - }, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Name: "route-2", - Namespace: "default", - Annotations: map[string]string{ - annotations.TargetKey: "10.1.1.3", - }, - }, - }, - }, - }, - } { - t.Run(tt.title, func(t *testing.T) { - kubeClient := fake.NewClientset() - for _, ocp := range tt.ocpRoute { - _, err := kubeClient.RouteV1().Routes(ocp.Namespace).Create(context.Background(), ocp, metav1.CreateOptions{}) - require.NoError(t, err) - } - - src, err := NewOcpRouteSource( - t.Context(), - kubeClient, - "", - "", - tt.fqdnTemplate, - !tt.combineFqdn, - false, - labels.Everything(), - "", - ) - require.NoError(t, err) - - endpoints, err := src.Endpoints(t.Context()) - require.NoError(t, err) - - validateEndpoints(t, endpoints, tt.expected) - }) - } -} diff --git a/source/openshift_route_test.go b/source/openshift_route_test.go index d9b1a9b578..c9b129f019 100644 --- a/source/openshift_route_test.go +++ b/source/openshift_route_test.go @@ -19,6 +19,7 @@ package source import ( "context" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -53,6 +54,7 @@ func (suite *OCPRouteSuite) SetupTest() { false, labels.Everything(), "", + time.Duration(0), ) suite.routeWithTargets = &routev1.Route{ @@ -152,6 +154,7 @@ func testOcpRouteSourceNewOcpRouteSource(t *testing.T) { false, labelSelector, "", + time.Duration(0), ) if ti.expectError { @@ -537,6 +540,7 @@ func testOcpRouteSourceEndpoints(t *testing.T) { false, labelSelector, tc.ocpRouterName, + time.Duration(0), ) require.NoError(t, err) diff --git a/source/pod.go b/source/pod.go index 50310fc204..6aac0e14c4 100644 --- a/source/pod.go +++ b/source/pod.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "text/template" + "time" log "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" @@ -70,6 +71,7 @@ func NewPodSource( combineFqdnAnnotation bool, annotationFilter string, labelSelector labels.Selector, + timeout time.Duration, ) (Source, error) { informerFactory := kubeinformers.NewSharedInformerFactoryWithOptions(kubeClient, 0, kubeinformers.WithNamespace(namespace)) podInformer := informerFactory.Core().V1().Pods() @@ -124,7 +126,7 @@ func NewPodSource( informerFactory.Start(ctx.Done()) // wait for the local cache to be populated. - if err := informers.WaitForCacheSync(ctx, informerFactory); err != nil { + if err := informers.WaitForCacheSync(ctx, informerFactory, timeout); err != nil { return nil, err } diff --git a/source/pod_fqdn_test.go b/source/pod_fqdn_test.go index eaabc35942..e07f3b575f 100644 --- a/source/pod_fqdn_test.go +++ b/source/pod_fqdn_test.go @@ -17,14 +17,14 @@ limitations under the License. package source import ( - "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/fake" "sigs.k8s.io/external-dns/endpoint" + "testing" + "time" ) func TestNewPodSourceWithFqdn(t *testing.T) { @@ -60,7 +60,8 @@ func TestNewPodSourceWithFqdn(t *testing.T) { tt.fqdnTemplate, false, "", - nil) + nil, + time.Duration(0)) if tt.expectError { assert.Error(t, err) @@ -385,48 +386,6 @@ func TestPodSourceFqdnTemplatingExamples(t *testing.T) { {DNSName: "pod-1.domain.tld", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"100.67.94.101"}}, }, }, - { - title: "fqdn templating with label conditional and kind check", - fqdnTemplate: `{{ if eq .Kind "Pod" }}{{ range $k, $v := .Labels }}{{ if and (contains $k "app") - (contains $v "my-service-") }}{{ $.Name }}.{{ $v }}.pod.tld.org{{ printf "," }}{{ end }}{{ end }}{{ end }}`, - expected: []*endpoint.Endpoint{ - {DNSName: "pod-1.my-service-1.pod.tld.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"100.67.94.101"}}, - {DNSName: "pod-2.my-service-2.pod.tld.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"100.67.94.102"}}, - }, - pods: []*v1.Pod{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "pod-1", - Namespace: "kube-system", - Labels: map[string]string{ - "app1": "my-service-1", - }, - }, - Status: v1.PodStatus{ - Phase: v1.PodRunning, - PodIP: "100.67.94.101", - PodIPs: []v1.PodIP{ - {IP: "100.67.94.101"}, - }, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Name: "pod-2", - Namespace: "kube-system", - Labels: map[string]string{ - "app2": "my-service-2", - }, - }, - Status: v1.PodStatus{ - Phase: v1.PodRunning, - PodIPs: []v1.PodIP{ - {IP: "100.67.94.102"}, - }, - }, - }, - }, - }, } { t.Run(tt.title, func(t *testing.T) { kubeClient := fake.NewClientset() @@ -451,7 +410,8 @@ func TestPodSourceFqdnTemplatingExamples(t *testing.T) { tt.fqdnTemplate, tt.combineFQDN, "", - nil) + nil, + time.Duration(0)) require.NoError(t, err) endpoints, err := src.Endpoints(t.Context()) @@ -515,7 +475,7 @@ func TestPodSourceFqdnTemplatingExamples_Failed(t *testing.T) { tt.fqdnTemplate, tt.combineFQDN, "", - nil) + nil, time.Duration(0)) require.NoError(t, err) _, err = src.Endpoints(t.Context()) diff --git a/source/pod_indexer_test.go b/source/pod_indexer_test.go index b74d1e1bc2..a28833df22 100644 --- a/source/pod_indexer_test.go +++ b/source/pod_indexer_test.go @@ -15,17 +15,17 @@ package source import ( "fmt" - "math/rand/v2" - "net" - "strconv" - "testing" - "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/fake" + "math/rand/v2" + "net" + "strconv" + "testing" + "time" "sigs.k8s.io/external-dns/source/annotations" ) @@ -221,7 +221,7 @@ func TestPodsWithAnnotationsAndLabels(t *testing.T) { tt.namespace, "", false, "", "{{ .Name }}.tld.org", false, - tt.annotationFilter, selector) + tt.annotationFilter, selector, time.Duration(0)) require.NoError(t, err) endpoints, err := pSource.Endpoints(t.Context()) diff --git a/source/pod_test.go b/source/pod_test.go index c4c8916d8f..d261f33188 100644 --- a/source/pod_test.go +++ b/source/pod_test.go @@ -19,9 +19,6 @@ package source import ( "context" "fmt" - "math/rand" - "testing" - log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -31,6 +28,9 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" corev1lister "k8s.io/client-go/listers/core/v1" "k8s.io/client-go/tools/cache" + "math/rand" + "testing" + "time" "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/internal/testutils" @@ -663,7 +663,7 @@ func TestPodSource(t *testing.T) { } } - client, err := NewPodSource(ctx, kubernetes, tc.targetNamespace, tc.compatibility, tc.ignoreNonHostNetworkPods, tc.PodSourceDomain, "", false, "", nil) + client, err := NewPodSource(ctx, kubernetes, tc.targetNamespace, tc.compatibility, tc.ignoreNonHostNetworkPods, tc.PodSourceDomain, "", false, "", nil, time.Duration(0)) require.NoError(t, err) endpoints, err := client.Endpoints(ctx) @@ -891,7 +891,7 @@ func TestPodSourceLogs(t *testing.T) { } } - client, err := NewPodSource(ctx, kubernetes, "", "", tc.ignoreNonHostNetworkPods, "", "", false, "", nil) + client, err := NewPodSource(ctx, kubernetes, "", "", tc.ignoreNonHostNetworkPods, "", "", false, "", nil, time.Duration(0)) require.NoError(t, err) hook := testutils.LogsUnderTestWithLogLevel(log.DebugLevel, t) @@ -1047,7 +1047,7 @@ func TestPodTransformerInPodSource(t *testing.T) { require.NoError(t, err) // Should not error when creating the source - src, err := NewPodSource(ctx, fakeClient, "", "", false, "", "", false, "", nil) + src, err := NewPodSource(ctx, fakeClient, "", "", false, "", "", false, "", nil, time.Duration(0)) require.NoError(t, err) ps, ok := src.(*podSource) require.True(t, ok) @@ -1128,7 +1128,7 @@ func TestPodTransformerInPodSource(t *testing.T) { require.NoError(t, err) // Should not error when creating the source - src, err := NewPodSource(ctx, fakeClient, "", "", false, "", "template", false, "", nil) + src, err := NewPodSource(ctx, fakeClient, "", "", false, "", "template", false, "", nil, time.Duration(0)) require.NoError(t, err) ps, ok := src.(*podSource) require.True(t, ok) diff --git a/source/service.go b/source/service.go index 07f563586e..474464afdd 100644 --- a/source/service.go +++ b/source/service.go @@ -25,6 +25,7 @@ import ( "sort" "strings" "text/template" + "time" log "github.com/sirupsen/logrus" v1 "k8s.io/api/core/v1" @@ -107,6 +108,7 @@ func NewServiceSource( labelSelector labels.Selector, resolveLoadBalancerHostname, listenEndpointEvents, exposeInternalIPv6, excludeUnschedulable bool, + timeout time.Duration, ) (Source, error) { tmpl, err := fqdn.ParseTemplate(fqdnTemplate) if err != nil { @@ -208,7 +210,7 @@ func NewServiceSource( informerFactory.Start(ctx.Done()) // wait for the local cache to be populated. - if err := informers.WaitForCacheSync(ctx, informerFactory); err != nil { + if err := informers.WaitForCacheSync(ctx, informerFactory, timeout); err != nil { return nil, err } diff --git a/source/service_fqdn_test.go b/source/service_fqdn_test.go index 63fce9cdb3..504353b9f8 100644 --- a/source/service_fqdn_test.go +++ b/source/service_fqdn_test.go @@ -15,8 +15,6 @@ package source import ( "fmt" - "testing" - "github.com/stretchr/testify/require" v1 "k8s.io/api/core/v1" discoveryv1 "k8s.io/api/discovery/v1" @@ -27,6 +25,8 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/internal/testutils" "sigs.k8s.io/external-dns/source/annotations" + "testing" + "time" ) func TestServiceSourceFqdnTemplatingExamples(t *testing.T) { @@ -738,94 +738,6 @@ func TestServiceSourceFqdnTemplatingExamples(t *testing.T) { {DNSName: "minecraft.host.tld", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"203.0.113.10"}}, }, }, - { - title: "templating resolves headless services with Kind check and label contains", - fqdnTemplate: `{{ if eq .Kind "Service" }}{{ range $key, $value := .Labels }} - {{ if and (contains $key "app") (contains $value "my-service-") }} - {{ $.Name }}.{{ $value }}.example.com,{{ end }}{{ end }}{{ end }}`, - expected: []*endpoint.Endpoint{ - {DNSName: "service-one.my-service-123.example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"100.66.2.241"}}, - {DNSName: "service-two.my-service-345.example.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"100.66.2.244"}}, - }, - services: []*v1.Service{ - { - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "service-one", - Labels: map[string]string{ - "app1": "my-service-123", - }, - }, - Spec: v1.ServiceSpec{ - Type: v1.ServiceTypeClusterIP, - ClusterIP: v1.ClusterIPNone, - IPFamilies: []v1.IPFamily{v1.IPv4Protocol}, - Ports: []v1.ServicePort{ - {Name: "http", Port: 8080}, - }, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "service-two", - Labels: map[string]string{ - "app2": "my-service-345", - }, - }, - Spec: v1.ServiceSpec{ - Type: v1.ServiceTypeClusterIP, - ClusterIP: v1.ClusterIPNone, - IPFamilies: []v1.IPFamily{v1.IPv4Protocol}, - Ports: []v1.ServicePort{ - {Name: "http", Port: 8080}, - }, - }, - }, - }, - endpointSlices: []*discoveryv1.EndpointSlice{ - { - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "service-one-xxxxx", - Labels: map[string]string{ - discoveryv1.LabelServiceName: "service-one", - }, - }, - AddressType: discoveryv1.AddressTypeIPv4, - Endpoints: []discoveryv1.Endpoint{ - { - Addresses: []string{"100.66.2.241"}, - TargetRef: &v1.ObjectReference{ - Kind: "Pod", - Name: "pod-1", - Namespace: "default", - }, - }, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - Name: "service-two-xxxxx", - Labels: map[string]string{ - discoveryv1.LabelServiceName: "service-two", - }, - }, - AddressType: discoveryv1.AddressTypeIPv4, - Endpoints: []discoveryv1.Endpoint{ - { - Addresses: []string{"100.66.2.244"}, - TargetRef: &v1.ObjectReference{ - Kind: "Pod", - Name: "pod-2", - Namespace: "default", - }, - }, - }, - }, - }, - }, } { t.Run(tt.title, func(t *testing.T) { kubeClient := fake.NewClientset() @@ -857,12 +769,7 @@ func TestServiceSourceFqdnTemplatingExamples(t *testing.T) { Namespace: el.Namespace, }, Spec: v1.PodSpec{ - Hostname: func() string { - if ep.Hostname != nil { - return *ep.Hostname - } - return "" - }(), + Hostname: *ep.Hostname, NodeName: "test-node", }, Status: v1.PodStatus{ @@ -891,6 +798,7 @@ func TestServiceSourceFqdnTemplatingExamples(t *testing.T) { false, true, true, + time.Duration(0), ) require.NoError(t, err) diff --git a/source/service_test.go b/source/service_test.go index 18886ebc18..4a524eb50b 100644 --- a/source/service_test.go +++ b/source/service_test.go @@ -100,6 +100,7 @@ func (suite *ServiceSuite) SetupTest() { false, false, false, + time.Duration(0), ) suite.NoError(err, "should initialize service source") } @@ -184,6 +185,7 @@ func testServiceSourceNewServiceSource(t *testing.T) { false, false, false, + time.Duration(0), ) if ti.expectError { @@ -1169,6 +1171,7 @@ func testServiceSourceEndpoints(t *testing.T) { false, false, false, + time.Duration(0), ) require.NoError(t, err) @@ -1386,6 +1389,7 @@ func testMultipleServicesEndpoints(t *testing.T) { false, false, false, + time.Duration(0), ) require.NoError(t, err) @@ -1692,6 +1696,7 @@ func TestClusterIpServices(t *testing.T) { false, false, false, + time.Duration(0), ) require.NoError(t, err) @@ -2520,6 +2525,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { false, tc.exposeInternalIPv6, tc.ignoreUnscheduledNodes, + time.Duration(0), ) require.NoError(t, err) @@ -3429,6 +3435,7 @@ func TestHeadlessServices(t *testing.T) { false, tc.exposeInternalIPv6, false, + time.Duration(0), ) require.NoError(t, err) @@ -3566,6 +3573,7 @@ func TestMultipleServicesPointingToSameLoadBalancer(t *testing.T) { false, false, true, + time.Duration(0), ) require.NoError(t, err) assert.NotNil(t, src) @@ -3933,6 +3941,7 @@ func TestMultipleHeadlessServicesPointingToPodsOnTheSameNode(t *testing.T) { false, false, true, + time.Duration(0), ) require.NoError(t, err) assert.NotNil(t, src) @@ -4392,6 +4401,7 @@ func TestHeadlessServicesHostIP(t *testing.T) { false, false, true, + time.Duration(0), ) require.NoError(t, err) @@ -4603,6 +4613,7 @@ func TestExternalServices(t *testing.T) { false, false, true, + time.Duration(0), ) require.NoError(t, err) @@ -4666,6 +4677,7 @@ func BenchmarkServiceEndpoints(b *testing.B) { false, false, true, + time.Duration(0), ) require.NoError(b, err) @@ -4766,6 +4778,7 @@ func TestNewServiceSourceInformersEnabled(t *testing.T) { false, false, false, + time.Duration(0), ) require.NoError(t, err) svcSrc, ok := svc.(*serviceSource) @@ -4798,6 +4811,7 @@ func TestNewServiceSourceWithServiceTypeFilters_Unsupported(t *testing.T) { false, false, false, + time.Duration(0), ) require.Errorf(t, err, "unsupported service type filter: \"UnknownType\". Supported types are: [\"ClusterIP\" \"NodePort\" \"LoadBalancer\" \"ExternalName\"]") require.Nil(t, svc, "ServiceSource should be nil when an unsupported service type is provided") @@ -4978,6 +4992,7 @@ func TestEndpointSlicesIndexer(t *testing.T) { false, false, true, + time.Duration(0), ) require.NoError(t, err) ss, ok := src.(*serviceSource) @@ -5066,6 +5081,7 @@ func TestPodTransformerInServiceSource(t *testing.T) { false, false, false, + time.Duration(0), ) require.NoError(t, err) ss, ok := src.(*serviceSource) diff --git a/source/store.go b/source/store.go index 49900c1c8b..07c57f112f 100644 --- a/source/store.go +++ b/source/store.go @@ -87,6 +87,7 @@ type Config struct { ServiceTypeFilter []string GlooNamespaces []string SkipperRouteGroupVersion string + CacheSyncTimeout time.Duration RequestTimeout time.Duration DefaultTargets []string ForceDefaultTargets bool @@ -140,6 +141,7 @@ func NewSourceConfig(cfg *externaldns.Config) *Config { ServiceTypeFilter: cfg.ServiceTypeFilter, GlooNamespaces: cfg.GlooNamespaces, SkipperRouteGroupVersion: cfg.SkipperRouteGroupVersion, + CacheSyncTimeout: cfg.CacheSyncTimeout, RequestTimeout: cfg.RequestTimeout, DefaultTargets: cfg.DefaultTargets, ForceDefaultTargets: cfg.ForceDefaultTargets, @@ -418,7 +420,7 @@ func buildNodeSource(ctx context.Context, p ClientGenerator, cfg *Config) (Sourc if err != nil { return nil, err } - return NewNodeSource(ctx, client, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.LabelFilter, cfg.ExposeInternalIPv6, cfg.ExcludeUnschedulable, cfg.CombineFQDNAndAnnotation) + return NewNodeSource(ctx, client, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.LabelFilter, cfg.ExposeInternalIPv6, cfg.ExcludeUnschedulable, cfg.CombineFQDNAndAnnotation, cfg.CacheSyncTimeout) } // buildServiceSource creates a Service source for exposing Kubernetes services as DNS records. @@ -433,7 +435,7 @@ func buildServiceSource(ctx context.Context, p ClientGenerator, cfg *Config) (So cfg.Compatibility, cfg.PublishInternal, cfg.PublishHostIP, cfg.AlwaysPublishNotReadyAddresses, cfg.ServiceTypeFilter, cfg.IgnoreHostnameAnnotation, cfg.LabelFilter, cfg.ResolveLoadBalancerHostname, cfg.ListenEndpointEvents, - cfg.ExposeInternalIPv6, cfg.ExcludeUnschedulable) + cfg.ExposeInternalIPv6, cfg.ExcludeUnschedulable, cfg.CacheSyncTimeout) } // buildIngressSource creates an Ingress source for exposing Kubernetes ingresses as DNS records. @@ -443,7 +445,7 @@ func buildIngressSource(ctx context.Context, p ClientGenerator, cfg *Config) (So 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 NewIngressSource(ctx, client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation, cfg.IgnoreIngressTLSSpec, cfg.IgnoreIngressRulesSpec, cfg.LabelFilter, cfg.IngressClassNames, cfg.CacheSyncTimeout) } // buildPodSource creates a Pod source for exposing Kubernetes pods as DNS records. @@ -453,7 +455,7 @@ func buildPodSource(ctx context.Context, p ClientGenerator, cfg *Config) (Source if err != nil { return nil, err } - return NewPodSource(ctx, client, cfg.Namespace, cfg.Compatibility, cfg.IgnoreNonHostNetworkPods, cfg.PodSourceDomain, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.AnnotationFilter, cfg.LabelFilter) + return NewPodSource(ctx, client, cfg.Namespace, cfg.Compatibility, cfg.IgnoreNonHostNetworkPods, cfg.PodSourceDomain, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.AnnotationFilter, cfg.LabelFilter, cfg.CacheSyncTimeout) } // buildIstioGatewaySource creates an Istio Gateway source for exposing Istio gateways as DNS records. @@ -467,7 +469,7 @@ func buildIstioGatewaySource(ctx context.Context, p ClientGenerator, cfg *Config if err != nil { return nil, err } - return NewIstioGatewaySource(ctx, kubernetesClient, istioClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation) + return NewIstioGatewaySource(ctx, kubernetesClient, istioClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation, cfg.CacheSyncTimeout) } // buildIstioVirtualServiceSource creates an Istio VirtualService source for exposing virtual services as DNS records. @@ -481,7 +483,7 @@ func buildIstioVirtualServiceSource(ctx context.Context, p ClientGenerator, cfg if err != nil { return nil, err } - return NewIstioVirtualServiceSource(ctx, kubernetesClient, istioClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation) + return NewIstioVirtualServiceSource(ctx, kubernetesClient, istioClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation, cfg.CacheSyncTimeout) } func buildAmbassadorHostSource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) { @@ -493,7 +495,7 @@ func buildAmbassadorHostSource(ctx context.Context, p ClientGenerator, cfg *Conf if err != nil { return nil, err } - return NewAmbassadorHostSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter, cfg.LabelFilter) + return NewAmbassadorHostSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter, cfg.LabelFilter, cfg.CacheSyncTimeout) } func buildContourHTTPProxySource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) { @@ -501,7 +503,7 @@ func buildContourHTTPProxySource(ctx context.Context, p ClientGenerator, cfg *Co if err != nil { return nil, err } - return NewContourHTTPProxySource(ctx, dynamicClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation) + return NewContourHTTPProxySource(ctx, dynamicClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation, cfg.CacheSyncTimeout) } // buildGlooProxySource creates a Gloo source for exposing Gloo proxies as DNS records. @@ -516,7 +518,7 @@ func buildGlooProxySource(ctx context.Context, p ClientGenerator, cfg *Config) ( if err != nil { return nil, err } - return NewGlooSource(ctx, dynamicClient, kubernetesClient, cfg.GlooNamespaces) + return NewGlooSource(ctx, dynamicClient, kubernetesClient, cfg.GlooNamespaces, cfg.CacheSyncTimeout) } func buildTraefikProxySource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) { @@ -528,7 +530,7 @@ func buildTraefikProxySource(ctx context.Context, p ClientGenerator, cfg *Config if err != nil { return nil, err } - return NewTraefikSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter, cfg.IgnoreHostnameAnnotation, cfg.TraefikEnableLegacy, cfg.TraefikDisableNew) + return NewTraefikSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter, cfg.IgnoreHostnameAnnotation, cfg.TraefikEnableLegacy, cfg.TraefikDisableNew, cfg.CacheSyncTimeout) } func buildOpenShiftRouteSource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) { @@ -536,7 +538,7 @@ func buildOpenShiftRouteSource(ctx context.Context, p ClientGenerator, cfg *Conf 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 NewOcpRouteSource(ctx, ocpClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation, cfg.LabelFilter, cfg.OCPRouterName, cfg.CacheSyncTimeout) } // buildCRDSource creates a CRD source for exposing custom resources as DNS records. @@ -547,11 +549,11 @@ func buildCRDSource(ctx context.Context, p ClientGenerator, cfg *Config) (Source if err != nil { return nil, err } - crdClient, scheme, err := NewCRDClientForAPIVersionKind(client, cfg.KubeConfig, cfg.APIServerURL, cfg.CRDSourceAPIVersion, cfg.CRDSourceKind) + crdClient, scheme, err := NewCRDClientForAPIVersionKind(client, cfg.KubeConfig, cfg.APIServerURL, cfg.CRDSourceAPIVersion, cfg.CRDSourceKind, cfg.CacheSyncTimeout) if err != nil { return nil, err } - return NewCRDSource(crdClient, cfg.Namespace, cfg.CRDSourceKind, cfg.AnnotationFilter, cfg.LabelFilter, scheme, cfg.UpdateEvents) + return NewCRDSource(crdClient, cfg.Namespace, cfg.CRDSourceKind, cfg.AnnotationFilter, cfg.LabelFilter, scheme, cfg.UpdateEvents, cfg.CacheSyncTimeout) } // buildSkipperRouteGroupSource creates a Skipper RouteGroup source for exposing route groups as DNS records. @@ -567,7 +569,7 @@ func buildSkipperRouteGroupSource(ctx context.Context, cfg *Config) (Source, err 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 NewRouteGroupSource(cfg.CacheSyncTimeout, 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) { @@ -579,7 +581,7 @@ func buildKongTCPIngressSource(ctx context.Context, p ClientGenerator, cfg *Conf if err != nil { return nil, err } - return NewKongTCPIngressSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter, cfg.IgnoreHostnameAnnotation) + return NewKongTCPIngressSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter, cfg.IgnoreHostnameAnnotation, cfg.CacheSyncTimeout) } func buildF5VirtualServerSource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) { @@ -591,7 +593,7 @@ func buildF5VirtualServerSource(ctx context.Context, p ClientGenerator, cfg *Con if err != nil { return nil, err } - return NewF5VirtualServerSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter) + return NewF5VirtualServerSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter, cfg.CacheSyncTimeout) } func buildF5TransportServerSource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) { @@ -603,7 +605,7 @@ func buildF5TransportServerSource(ctx context.Context, p ClientGenerator, cfg *C if err != nil { return nil, err } - return NewF5TransportServerSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter) + return NewF5TransportServerSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter, cfg.CacheSyncTimeout) } // instrumentedRESTConfig creates a REST config with request instrumentation for monitoring. diff --git a/source/traefik_proxy.go b/source/traefik_proxy.go index c884a6d5fb..ca428d1eee 100644 --- a/source/traefik_proxy.go +++ b/source/traefik_proxy.go @@ -23,6 +23,7 @@ import ( "regexp" "sort" "strings" + "time" log "github.com/sirupsen/logrus" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -108,6 +109,7 @@ func NewTraefikSource( kubeClient kubernetes.Interface, namespace, annotationFilter string, ignoreHostnameAnnotation, enableLegacy, disableNew bool, + timeout time.Duration, ) (Source, error) { // Use shared informer to listen for add/update/delete of Host in the specified namespace. // Set resync period to 0, to prevent processing when nothing has changed. @@ -160,7 +162,7 @@ func NewTraefikSource( informerFactory.Start(ctx.Done()) // wait for the local cache to be populated. - if err := informers.WaitForDynamicCacheSync(ctx, informerFactory); err != nil { + if err := informers.WaitForDynamicCacheSync(ctx, informerFactory, timeout); err != nil { return nil, err } diff --git a/source/traefik_proxy_test.go b/source/traefik_proxy_test.go index abce58cbc5..67d476fb59 100644 --- a/source/traefik_proxy_test.go +++ b/source/traefik_proxy_test.go @@ -20,12 +20,12 @@ import ( "context" "encoding/json" "fmt" - "testing" - "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/tools/cache" + "testing" + "time" "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -354,7 +354,7 @@ func TestTraefikProxyIngressRouteEndpoints(t *testing.T) { _, err = fakeDynamicClient.Resource(ingressRouteGVR).Namespace(defaultTraefikNamespace).Create(context.Background(), &ir, metav1.CreateOptions{}) assert.NoError(t, err) - source, err := NewTraefikSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, defaultTraefikNamespace, "kubernetes.io/ingress.class=traefik", ti.ignoreHostnameAnnotation, false, false) + source, err := NewTraefikSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, defaultTraefikNamespace, "kubernetes.io/ingress.class=traefik", ti.ignoreHostnameAnnotation, false, false, time.Duration(0)) assert.NoError(t, err) assert.NotNil(t, source) @@ -647,7 +647,7 @@ func TestTraefikProxyIngressRouteTCPEndpoints(t *testing.T) { _, err = fakeDynamicClient.Resource(ingressRouteTCPGVR).Namespace(defaultTraefikNamespace).Create(context.Background(), &ir, metav1.CreateOptions{}) require.NoError(t, err) - source, err := NewTraefikSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, defaultTraefikNamespace, "kubernetes.io/ingress.class=traefik", ti.ignoreHostnameAnnotation, false, false) + source, err := NewTraefikSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, defaultTraefikNamespace, "kubernetes.io/ingress.class=traefik", ti.ignoreHostnameAnnotation, false, false, time.Duration(0)) require.NoError(t, err) assert.NotNil(t, source) @@ -788,7 +788,7 @@ func TestTraefikProxyIngressRouteUDPEndpoints(t *testing.T) { _, err = fakeDynamicClient.Resource(ingressRouteUDPGVR).Namespace(defaultTraefikNamespace).Create(context.Background(), &ir, metav1.CreateOptions{}) assert.NoError(t, err) - source, err := NewTraefikSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, defaultTraefikNamespace, "kubernetes.io/ingress.class=traefik", ti.ignoreHostnameAnnotation, false, false) + source, err := NewTraefikSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, defaultTraefikNamespace, "kubernetes.io/ingress.class=traefik", ti.ignoreHostnameAnnotation, false, false, time.Duration(0)) assert.NoError(t, err) assert.NotNil(t, source) @@ -1117,7 +1117,7 @@ func TestTraefikProxyOldIngressRouteEndpoints(t *testing.T) { _, err = fakeDynamicClient.Resource(oldIngressRouteGVR).Namespace(defaultTraefikNamespace).Create(context.Background(), &ir, metav1.CreateOptions{}) assert.NoError(t, err) - source, err := NewTraefikSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, defaultTraefikNamespace, "kubernetes.io/ingress.class=traefik", ti.ignoreHostnameAnnotation, true, false) + source, err := NewTraefikSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, defaultTraefikNamespace, "kubernetes.io/ingress.class=traefik", ti.ignoreHostnameAnnotation, true, false, time.Duration(0)) assert.NoError(t, err) assert.NotNil(t, source) @@ -1410,7 +1410,7 @@ func TestTraefikProxyOldIngressRouteTCPEndpoints(t *testing.T) { _, err = fakeDynamicClient.Resource(oldIngressRouteTCPGVR).Namespace(defaultTraefikNamespace).Create(context.Background(), &ir, metav1.CreateOptions{}) assert.NoError(t, err) - source, err := NewTraefikSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, defaultTraefikNamespace, "kubernetes.io/ingress.class=traefik", ti.ignoreHostnameAnnotation, true, false) + source, err := NewTraefikSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, defaultTraefikNamespace, "kubernetes.io/ingress.class=traefik", ti.ignoreHostnameAnnotation, true, false, time.Duration(0)) assert.NoError(t, err) assert.NotNil(t, source) @@ -1551,7 +1551,7 @@ func TestTraefikProxyOldIngressRouteUDPEndpoints(t *testing.T) { _, err = fakeDynamicClient.Resource(oldIngressRouteUDPGVR).Namespace(defaultTraefikNamespace).Create(context.Background(), &ir, metav1.CreateOptions{}) assert.NoError(t, err) - source, err := NewTraefikSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, defaultTraefikNamespace, "kubernetes.io/ingress.class=traefik", ti.ignoreHostnameAnnotation, true, false) + source, err := NewTraefikSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, defaultTraefikNamespace, "kubernetes.io/ingress.class=traefik", ti.ignoreHostnameAnnotation, true, false, time.Duration(0)) assert.NoError(t, err) assert.NotNil(t, source) @@ -1713,7 +1713,7 @@ func TestTraefikAPIGroupFlags(t *testing.T) { _, err = fakeDynamicClient.Resource(ti.gvr).Namespace(defaultTraefikNamespace).Create(context.Background(), &ir, metav1.CreateOptions{}) assert.NoError(t, err) - source, err := NewTraefikSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, defaultTraefikNamespace, "kubernetes.io/ingress.class=traefik", ti.ignoreHostnameAnnotation, ti.enableLegacy, ti.disableNew) + source, err := NewTraefikSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, defaultTraefikNamespace, "kubernetes.io/ingress.class=traefik", ti.ignoreHostnameAnnotation, ti.enableLegacy, ti.disableNew, time.Duration(0)) assert.NoError(t, err) assert.NotNil(t, source)