From 56cca8716312a01fb18c8c0c8da2026606c6cea7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandor=20Sz=C3=BCcs?= Date: Sun, 29 Jun 2025 22:02:34 +0200 Subject: [PATCH 01/49] fix: reduce warning by using idna profile (#5587) fixes https://github.com/kubernetes-sigs/external-dns/issues/5581, tested by running test with -v and added println("warn") to the log warning path Signed-off-by: Sandor Szuecs --- plan/plan.go | 8 +++++++- plan/plan_test.go | 20 +++++++++++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/plan/plan.go b/plan/plan.go index 699dcdc5c..eb91bb4ec 100644 --- a/plan/plan.go +++ b/plan/plan.go @@ -339,10 +339,16 @@ func filterRecordsForPlan(records []*endpoint.Endpoint, domainFilter endpoint.Ma return filtered } +var idnaProfile = idna.New( + idna.MapForLookup(), + idna.Transitional(true), + idna.StrictDomainName(false), +) + // normalizeDNSName converts a DNS name to a canonical form, so that we can use string equality // it: removes space, get ASCII version of dnsName complient with Section 5 of RFC 5891, ensures there is a trailing dot func normalizeDNSName(dnsName string) string { - s, err := idna.Lookup.ToASCII(strings.TrimSpace(dnsName)) + s, err := idnaProfile.ToASCII(strings.TrimSpace(dnsName)) if err != nil { log.Warnf(`Got error while parsing DNSName %s: %v`, dnsName, err) } diff --git a/plan/plan_test.go b/plan/plan_test.go index 5dde2c9c9..c4278087e 100644 --- a/plan/plan_test.go +++ b/plan/plan_test.go @@ -1028,7 +1028,7 @@ func validateEntries(t *testing.T, entries, expected []*endpoint.Endpoint) { } } -func TestNormalizeDNSName(t *testing.T) { +func TestNormalizeDNSName(tt *testing.T) { records := []struct { dnsName string expect string @@ -1061,6 +1061,18 @@ func TestNormalizeDNSName(t *testing.T) { "foo.com.", "foo.com.", }, + { + "_foo.com.", + "_foo.com.", + }, + { + "\u005Ffoo.com.", + "_foo.com.", + }, + { + ".foo.com.", + ".foo.com.", + }, { "foo123.COM", "foo123.com.", @@ -1099,8 +1111,10 @@ func TestNormalizeDNSName(t *testing.T) { }, } for _, r := range records { - gotName := normalizeDNSName(r.dnsName) - assert.Equal(t, r.expect, gotName) + tt.Run(r.dnsName, func(t *testing.T) { + gotName := normalizeDNSName(r.dnsName) + assert.Equal(t, r.expect, gotName) + }) } } From 92f636ab3db7cc975ad3441287d9c7390f8cb55e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandor=20Sz=C3=BCcs?= Date: Sun, 29 Jun 2025 22:26:30 +0200 Subject: [PATCH 02/49] fix: zonefinder used a quoted version of %s, but we should use %q instead (#5588) change: test message to fit %q Signed-off-by: Sandor Szuecs --- provider/zonefinder.go | 10 +++++----- provider/zonefinder_test.go | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/provider/zonefinder.go b/provider/zonefinder.go index a81f997b3..a47060bc4 100644 --- a/provider/zonefinder.go +++ b/provider/zonefinder.go @@ -43,19 +43,19 @@ func (z ZoneIDName) Add(zoneID, zoneName string) { // ensures compatibility with such use cases. func (z ZoneIDName) FindZone(hostname string) (suitableZoneID, suitableZoneName string) { var name string - domain_labels := strings.Split(hostname, ".") - for i, label := range domain_labels { + domainLabels := strings.Split(hostname, ".") + for i, label := range domainLabels { if strings.Contains(label, "_") { continue } convertedLabel, err := idna.Lookup.ToUnicode(label) if err != nil { - log.Warnf("Failed to convert label '%s' of hostname '%s' to its Unicode form: %v", label, hostname, err) + log.Warnf("Failed to convert label %q of hostname %q to its Unicode form: %v", label, hostname, err) convertedLabel = label } - domain_labels[i] = convertedLabel + domainLabels[i] = convertedLabel } - name = strings.Join(domain_labels, ".") + name = strings.Join(domainLabels, ".") for zoneID, zoneName := range z { if name == zoneName || strings.HasSuffix(name, "."+zoneName) { diff --git a/provider/zonefinder_test.go b/provider/zonefinder_test.go index b5afb047f..dccfe5adb 100644 --- a/provider/zonefinder_test.go +++ b/provider/zonefinder_test.go @@ -80,5 +80,5 @@ func TestZoneIDName(t *testing.T) { hook := testutils.LogsUnderTestWithLogLevel(log.WarnLevel, t) _, _ = z.FindZone("???") - testutils.TestHelperLogContains("Failed to convert label '???' of hostname '???' to its Unicode form: idna: disallowed rune U+003F", hook, t) + testutils.TestHelperLogContains("Failed to convert label \"???\" of hostname \"???\" to its Unicode form: idna: disallowed rune U+003F", hook, t) } From 93c76553e78c4016a82d834d848cb4d413863dad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandor=20Sz=C3=BCcs?= Date: Sun, 29 Jun 2025 22:26:37 +0200 Subject: [PATCH 03/49] refactor: use slices.Contains instead of handrolled for loop (#5589) Signed-off-by: Sandor Szuecs --- plan/plan.go | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/plan/plan.go b/plan/plan.go index eb91bb4ec..8061a5b3f 100644 --- a/plan/plan.go +++ b/plan/plan.go @@ -18,6 +18,7 @@ package plan import ( "fmt" + "slices" "strings" "github.com/google/go-cmp/cmp" @@ -359,15 +360,8 @@ func normalizeDNSName(dnsName string) string { } func IsManagedRecord(record string, managedRecords, excludeRecords []string) bool { - for _, r := range excludeRecords { - if record == r { - return false - } + if slices.Contains(excludeRecords, record) { + return false } - for _, r := range managedRecords { - if record == r { - return true - } - } - return false + return slices.Contains(managedRecords, record) } From e467e60335ff9b79d15d3d656579be5eaac5f2c9 Mon Sep 17 00:00:00 2001 From: Andrew Hay <39sumer3939@gmail.com> Date: Sun, 29 Jun 2025 16:58:30 -0400 Subject: [PATCH 04/49] chore(store*): add reduce complexity and improve code coverage (#5568) * chore(store*): add reduce complexity and improve code coverage * docs(store.go): reasoning for helper function * style(store): standardized order of args * chore: gofmt auto-format source/store.go for lint compliance --- .golangci.yml | 2 +- source/store.go | 466 +++++++++++++++++++++++++++++++------------ source/store_test.go | 34 ++++ 3 files changed, 368 insertions(+), 134 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index c67a5494e..d03e3cda8 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -40,7 +40,7 @@ linters: - name: confusing-naming disabled: true cyclop: # Lower cyclomatic complexity threshold after the max complexity is lowered - max-complexity: 51 + max-complexity: 44 testifylint: # Enable all checkers (https://github.com/Antonboom/testifylint#checkers). # Default: false diff --git a/source/store.go b/source/store.go index 82a03f1b9..cd458a0c8 100644 --- a/source/store.go +++ b/source/store.go @@ -45,6 +45,20 @@ import ( var ErrSourceNotFound = errors.New("source not found") // Config holds shared configuration options for all Sources. +// This struct centralizes all source-related configuration to avoid parameter proliferation +// in individual source constructors. It follows the configuration pattern where a single +// config object is passed rather than individual parameters. +// +// Common Configuration Fields: +// - Namespace: Target namespace for source operations +// - AnnotationFilter: Filter sources by annotation patterns +// - LabelFilter: Filter sources by label selectors +// - FQDNTemplate: Template for generating fully qualified domain names +// - CombineFQDNAndAnnotation: Whether to combine FQDN template with annotations +// - IgnoreHostnameAnnotation: Whether to ignore hostname annotations +// +// The config is created from externaldns.Config via NewSourceConfig() which handles +// type conversions and validation. type Config struct { Namespace string AnnotationFilter string @@ -135,7 +149,21 @@ func NewSourceConfig(cfg *externaldns.Config) *Config { } } -// ClientGenerator provides clients +// ClientGenerator provides clients for various Kubernetes APIs and external services. +// This interface abstracts client creation and enables dependency injection for testing. +// It uses the singleton pattern to ensure only one instance of each client is created +// and reused across multiple source instances. +// +// Supported Client Types: +// - KubeClient: Standard Kubernetes API client +// - GatewayClient: Gateway API client for Gateway resources +// - IstioClient: Istio service mesh client +// - CloudFoundryClient: CloudFoundry platform client +// - DynamicKubernetesClient: Dynamic client for custom resources +// - OpenShiftClient: OpenShift-specific client for Route resources +// +// The singleton behavior is implemented in SingletonClientGenerator which uses +// sync.Once to guarantee single initialization of each client type. type ClientGenerator interface { KubeClient() (kubernetes.Interface, error) GatewayClient() (gateway.Interface, error) @@ -145,8 +173,17 @@ type ClientGenerator interface { OpenShiftClient() (openshift.Interface, error) } -// SingletonClientGenerator stores provider clients and guarantees that only one instance of client -// will be generated +// SingletonClientGenerator stores provider clients and guarantees that only one instance of each client +// will be generated throughout the application lifecycle. +// +// Thread Safety: Uses sync.Once for each client type to ensure thread-safe initialization. +// This is important because external-dns may create multiple sources concurrently. +// +// Memory Efficiency: Prevents creating multiple instances of expensive client objects +// that maintain their own connection pools and caches. +// +// Configuration: Clients are configured using KubeConfig, APIServerURL, and RequestTimeout +// which are set during SingletonClientGenerator initialization. type SingletonClientGenerator struct { KubeConfig string APIServerURL string @@ -261,33 +298,47 @@ func ByNames(ctx context.Context, p ClientGenerator, names []string, cfg *Config return sources, nil } -// BuildWithConfig allows generating a Source implementation from the shared config +// BuildWithConfig creates a Source implementation using the factory pattern. +// This function serves as the central registry for all available source types. +// +// Source Selection: Uses a string identifier to determine which source type to create. +// This allows for runtime configuration and easy extension with new source types. +// +// Error Handling: Returns ErrSourceNotFound for unsupported source types, +// allowing callers to handle unknown sources gracefully. +// +// Supported Source Types: +// - "node": Kubernetes nodes +// - "service": Kubernetes services +// - "ingress": Kubernetes ingresses +// - "pod": Kubernetes pods +// - "gateway-*": Gateway API resources (httproute, grpcroute, tlsroute, tcproute, udproute) +// - "istio-*": Istio resources (gateway, virtualservice) +// - "cloudfoundry": CloudFoundry applications +// - "ambassador-host": Ambassador Host resources +// - "contour-httpproxy": Contour HTTPProxy resources +// - "gloo-proxy": Gloo proxy resources +// - "traefik-proxy": Traefik proxy resources +// - "openshift-route": OpenShift Route resources +// - "crd": Custom Resource Definitions +// - "skipper-routegroup": Skipper RouteGroup resources +// - "kong-tcpingress": Kong TCP Ingress resources +// - "f5-*": F5 resources (virtualserver, transportserver) +// - "fake": Fake source for testing +// - "connector": Connector source for external systems +// +// Design Note: Gateway API sources use a different pattern (direct constructor calls) +// because they have simpler initialization requirements. func BuildWithConfig(ctx context.Context, source string, p ClientGenerator, cfg *Config) (Source, error) { switch source { case "node": - client, err := p.KubeClient() - if err != nil { - return nil, err - } - return NewNodeSource(ctx, client, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.LabelFilter, cfg.ExposeInternalIPv6, cfg.ExcludeUnschedulable, cfg.CombineFQDNAndAnnotation) + return buildNodeSource(ctx, p, cfg) case "service": - client, err := p.KubeClient() - if err != nil { - return nil, err - } - return NewServiceSource(ctx, client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.Compatibility, cfg.PublishInternal, cfg.PublishHostIP, cfg.AlwaysPublishNotReadyAddresses, cfg.ServiceTypeFilter, cfg.IgnoreHostnameAnnotation, cfg.LabelFilter, cfg.ResolveLoadBalancerHostname, cfg.ListenEndpointEvents, cfg.ExposeInternalIPv6) + return buildServiceSource(ctx, p, cfg) case "ingress": - client, err := p.KubeClient() - if err != nil { - return nil, err - } - return NewIngressSource(ctx, client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation, cfg.IgnoreIngressTLSSpec, cfg.IgnoreIngressRulesSpec, cfg.LabelFilter, cfg.IngressClassNames) + return buildIngressSource(ctx, p, cfg) case "pod": - client, err := p.KubeClient() - if err != nil { - return nil, err - } - return NewPodSource(ctx, client, cfg.Namespace, cfg.Compatibility, cfg.IgnoreNonHostNetworkPods, cfg.PodSourceDomain, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation) + return buildPodSource(ctx, p, cfg) case "gateway-httproute": return NewGatewayHTTPRouteSource(p, cfg) case "gateway-grpcroute": @@ -299,133 +350,274 @@ func BuildWithConfig(ctx context.Context, source string, p ClientGenerator, cfg case "gateway-udproute": return NewGatewayUDPRouteSource(p, cfg) case "istio-gateway": - kubernetesClient, err := p.KubeClient() - if err != nil { - return nil, err - } - istioClient, err := p.IstioClient() - if err != nil { - return nil, err - } - return NewIstioGatewaySource(ctx, kubernetesClient, istioClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation) + return buildIstioGatewaySource(ctx, p, cfg) case "istio-virtualservice": - kubernetesClient, err := p.KubeClient() - if err != nil { - return nil, err - } - istioClient, err := p.IstioClient() - if err != nil { - return nil, err - } - return NewIstioVirtualServiceSource(ctx, kubernetesClient, istioClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation) + return buildIstioVirtualServiceSource(ctx, p, cfg) case "cloudfoundry": - cfClient, err := p.CloudFoundryClient(cfg.CFAPIEndpoint, cfg.CFUsername, cfg.CFPassword) - if err != nil { - return nil, err - } - return NewCloudFoundrySource(cfClient) + return buildCloudFoundrySource(ctx, p, cfg) case "ambassador-host": - kubernetesClient, err := p.KubeClient() - if err != nil { - return nil, err - } - dynamicClient, err := p.DynamicKubernetesClient() - if err != nil { - return nil, err - } - return NewAmbassadorHostSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter, cfg.LabelFilter) + return buildAmbassadorHostSource(ctx, p, cfg) case "contour-httpproxy": - dynamicClient, err := p.DynamicKubernetesClient() - if err != nil { - return nil, err - } - return NewContourHTTPProxySource(ctx, dynamicClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation) + return buildContourHTTPProxySource(ctx, p, cfg) case "gloo-proxy": - kubernetesClient, err := p.KubeClient() - if err != nil { - return nil, err - } - dynamicClient, err := p.DynamicKubernetesClient() - if err != nil { - return nil, err - } - return NewGlooSource(dynamicClient, kubernetesClient, cfg.GlooNamespaces) + return buildGlooProxySource(ctx, p, cfg) case "traefik-proxy": - kubernetesClient, err := p.KubeClient() - if err != nil { - return nil, err - } - dynamicClient, err := p.DynamicKubernetesClient() - if err != nil { - return nil, err - } - return NewTraefikSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter, cfg.IgnoreHostnameAnnotation, cfg.TraefikDisableLegacy, cfg.TraefikDisableNew) + return buildTraefikProxySource(ctx, p, cfg) case "openshift-route": - ocpClient, err := p.OpenShiftClient() - if err != nil { - return nil, err - } - return NewOcpRouteSource(ctx, ocpClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation, cfg.LabelFilter, cfg.OCPRouterName) + return buildOpenShiftRouteSource(ctx, p, cfg) case "fake": return NewFakeSource(cfg.FQDNTemplate) case "connector": return NewConnectorSource(cfg.ConnectorServer) case "crd": - client, err := p.KubeClient() - if err != nil { - return nil, err - } - crdClient, scheme, err := NewCRDClientForAPIVersionKind(client, cfg.KubeConfig, cfg.APIServerURL, cfg.CRDSourceAPIVersion, cfg.CRDSourceKind) - if err != nil { - return nil, err - } - return NewCRDSource(crdClient, cfg.Namespace, cfg.CRDSourceKind, cfg.AnnotationFilter, cfg.LabelFilter, scheme, cfg.UpdateEvents) + return buildCRDSource(ctx, p, cfg) case "skipper-routegroup": - apiServerURL := cfg.APIServerURL - tokenPath := "" - token := "" - restConfig, err := GetRestConfig(cfg.KubeConfig, cfg.APIServerURL) - if err == nil { - apiServerURL = restConfig.Host - tokenPath = restConfig.BearerTokenFile - token = restConfig.BearerToken - } - return NewRouteGroupSource(cfg.RequestTimeout, token, tokenPath, apiServerURL, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.SkipperRouteGroupVersion, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation) + return buildSkipperRouteGroupSource(ctx, cfg) case "kong-tcpingress": - kubernetesClient, err := p.KubeClient() - if err != nil { - return nil, err - } - dynamicClient, err := p.DynamicKubernetesClient() - if err != nil { - return nil, err - } - return NewKongTCPIngressSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter, cfg.IgnoreHostnameAnnotation) + return buildKongTCPIngressSource(ctx, p, cfg) case "f5-virtualserver": - kubernetesClient, err := p.KubeClient() - if err != nil { - return nil, err - } - dynamicClient, err := p.DynamicKubernetesClient() - if err != nil { - return nil, err - } - return NewF5VirtualServerSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter) + return buildF5VirtualServerSource(ctx, p, cfg) case "f5-transportserver": - kubernetesClient, err := p.KubeClient() - if err != nil { - return nil, err - } - dynamicClient, err := p.DynamicKubernetesClient() - if err != nil { - return nil, err - } - return NewF5TransportServerSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter) + return buildF5TransportServerSource(ctx, p, cfg) } - return nil, ErrSourceNotFound } +// Source Builder Functions +// +// The following functions follow a standardized pattern for creating source instances. +// This standardization improves code consistency, maintainability, and readability. +// +// Standardized Function Signature Pattern: +// +// func buildXXXSource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) +// +// Standardized Constructor Parameter Pattern (where applicable): +// 1. ctx (context.Context) - Always first when supported by the source constructor +// 2. client(s) (kubernetes.Interface, dynamic.Interface, etc.) - Kubernetes clients +// 3. namespace (string) - Target namespace for the source +// 4. annotationFilter (string) - Filter for annotations +// 5. labelFilter (labels.Selector) - Filter for labels (when applicable) +// 6. fqdnTemplate (string) - FQDN template for DNS record generation +// 7. combineFQDNAndAnnotation (bool) - Whether to combine FQDN template with annotations +// 8. ...other parameters - Source-specific parameters in logical order +// +// Design Principles: +// - Each source type has its own specific requirements and dependencies +// - Separating build functions allows for clearer code organization and easier maintenance +// - Individual functions enable straightforward error handling and independent testing +// - Modularity makes it easier to add new source types or modify existing ones +// - Consistent parameter ordering reduces cognitive load when working with multiple sources +// +// Note: Some sources may deviate from the standard pattern due to their unique requirements +// (e.g., RouteGroupSource doesn't use ClientGenerator, GlooSource doesn't accept context) +// buildNodeSource creates a Node source for exposing node information as DNS records. +// Follows standard pattern: ctx, client, annotationFilter, fqdnTemplate, labelFilter, ...other +func buildNodeSource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) { + client, err := p.KubeClient() + if err != nil { + return nil, err + } + return NewNodeSource(ctx, client, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.LabelFilter, cfg.ExposeInternalIPv6, cfg.ExcludeUnschedulable, cfg.CombineFQDNAndAnnotation) +} + +// buildServiceSource creates a Service source for exposing Kubernetes services as DNS records. +// Follows standard pattern: ctx, client, namespace, annotationFilter, fqdnTemplate, ...other +func buildServiceSource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) { + client, err := p.KubeClient() + if err != nil { + return nil, err + } + return NewServiceSource(ctx, client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.Compatibility, cfg.PublishInternal, cfg.PublishHostIP, cfg.AlwaysPublishNotReadyAddresses, cfg.ServiceTypeFilter, cfg.IgnoreHostnameAnnotation, cfg.LabelFilter, cfg.ResolveLoadBalancerHostname, cfg.ListenEndpointEvents, cfg.ExposeInternalIPv6) +} + +// buildIngressSource creates an Ingress source for exposing Kubernetes ingresses as DNS records. +// Follows standard pattern: ctx, client, namespace, annotationFilter, fqdnTemplate, ...other +func buildIngressSource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) { + client, err := p.KubeClient() + if err != nil { + return nil, err + } + return NewIngressSource(ctx, client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation, cfg.IgnoreIngressTLSSpec, cfg.IgnoreIngressRulesSpec, cfg.LabelFilter, cfg.IngressClassNames) +} + +// buildPodSource creates a Pod source for exposing Kubernetes pods as DNS records. +// Follows standard pattern: ctx, client, namespace, ...other (no annotation/label filters) +func buildPodSource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) { + client, err := p.KubeClient() + if err != nil { + return nil, err + } + return NewPodSource(ctx, client, cfg.Namespace, cfg.Compatibility, cfg.IgnoreNonHostNetworkPods, cfg.PodSourceDomain, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation) +} + +// buildIstioGatewaySource creates an Istio Gateway source for exposing Istio gateways as DNS records. +// Requires both Kubernetes and Istio clients. Follows standard parameter pattern. +func buildIstioGatewaySource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) { + kubernetesClient, err := p.KubeClient() + if err != nil { + return nil, err + } + istioClient, err := p.IstioClient() + if err != nil { + return nil, err + } + return NewIstioGatewaySource(ctx, kubernetesClient, istioClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation) +} + +// buildIstioVirtualServiceSource creates an Istio VirtualService source for exposing virtual services as DNS records. +// Requires both Kubernetes and Istio clients. Follows standard parameter pattern. +func buildIstioVirtualServiceSource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) { + kubernetesClient, err := p.KubeClient() + if err != nil { + return nil, err + } + istioClient, err := p.IstioClient() + if err != nil { + return nil, err + } + return NewIstioVirtualServiceSource(ctx, kubernetesClient, istioClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation) +} + +// buildCloudFoundrySource creates a CloudFoundry source for exposing CF applications as DNS records. +// Uses CloudFoundry client instead of Kubernetes client. Simple constructor with minimal parameters. +func buildCloudFoundrySource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) { + cfClient, err := p.CloudFoundryClient(cfg.CFAPIEndpoint, cfg.CFUsername, cfg.CFPassword) + if err != nil { + return nil, err + } + return NewCloudFoundrySource(cfClient) +} + +func buildAmbassadorHostSource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) { + kubernetesClient, err := p.KubeClient() + if err != nil { + return nil, err + } + dynamicClient, err := p.DynamicKubernetesClient() + if err != nil { + return nil, err + } + return NewAmbassadorHostSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter, cfg.LabelFilter) +} + +func buildContourHTTPProxySource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) { + dynamicClient, err := p.DynamicKubernetesClient() + if err != nil { + return nil, err + } + return NewContourHTTPProxySource(ctx, dynamicClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation) +} + +// buildGlooProxySource creates a Gloo source for exposing Gloo proxies as DNS records. +// Requires both dynamic and standard Kubernetes clients. +// Note: Does not accept context parameter in constructor (legacy design). +func buildGlooProxySource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) { + kubernetesClient, err := p.KubeClient() + if err != nil { + return nil, err + } + dynamicClient, err := p.DynamicKubernetesClient() + if err != nil { + return nil, err + } + return NewGlooSource(dynamicClient, kubernetesClient, cfg.GlooNamespaces) +} + +func buildTraefikProxySource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) { + kubernetesClient, err := p.KubeClient() + if err != nil { + return nil, err + } + dynamicClient, err := p.DynamicKubernetesClient() + if err != nil { + return nil, err + } + return NewTraefikSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter, cfg.IgnoreHostnameAnnotation, cfg.TraefikDisableLegacy, cfg.TraefikDisableNew) +} + +func buildOpenShiftRouteSource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) { + ocpClient, err := p.OpenShiftClient() + if err != nil { + return nil, err + } + return NewOcpRouteSource(ctx, ocpClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation, cfg.LabelFilter, cfg.OCPRouterName) +} + +// buildCRDSource creates a CRD source for exposing custom resources as DNS records. +// Uses a specialized CRD client created via NewCRDClientForAPIVersionKind. +// Parameter order: crdClient, namespace, kind, annotationFilter, labelFilter, scheme, updateEvents +func buildCRDSource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) { + client, err := p.KubeClient() + if err != nil { + return nil, err + } + crdClient, scheme, err := NewCRDClientForAPIVersionKind(client, cfg.KubeConfig, cfg.APIServerURL, cfg.CRDSourceAPIVersion, cfg.CRDSourceKind) + if err != nil { + return nil, err + } + return NewCRDSource(crdClient, cfg.Namespace, cfg.CRDSourceKind, cfg.AnnotationFilter, cfg.LabelFilter, scheme, cfg.UpdateEvents) +} + +// buildSkipperRouteGroupSource creates a Skipper RouteGroup source for exposing route groups as DNS records. +// Special case: Does not use ClientGenerator pattern, instead manages its own authentication. +// Retrieves bearer token from REST config for API server authentication. +func buildSkipperRouteGroupSource(ctx context.Context, cfg *Config) (Source, error) { + apiServerURL := cfg.APIServerURL + tokenPath := "" + token := "" + restConfig, err := GetRestConfig(cfg.KubeConfig, cfg.APIServerURL) + if err == nil { + apiServerURL = restConfig.Host + tokenPath = restConfig.BearerTokenFile + token = restConfig.BearerToken + } + return NewRouteGroupSource(cfg.RequestTimeout, token, tokenPath, apiServerURL, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.SkipperRouteGroupVersion, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation) +} + +func buildKongTCPIngressSource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) { + kubernetesClient, err := p.KubeClient() + if err != nil { + return nil, err + } + dynamicClient, err := p.DynamicKubernetesClient() + if err != nil { + return nil, err + } + return NewKongTCPIngressSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter, cfg.IgnoreHostnameAnnotation) +} + +func buildF5VirtualServerSource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) { + kubernetesClient, err := p.KubeClient() + if err != nil { + return nil, err + } + dynamicClient, err := p.DynamicKubernetesClient() + if err != nil { + return nil, err + } + return NewF5VirtualServerSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter) +} + +func buildF5TransportServerSource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) { + kubernetesClient, err := p.KubeClient() + if err != nil { + return nil, err + } + dynamicClient, err := p.DynamicKubernetesClient() + if err != nil { + return nil, err + } + return NewF5TransportServerSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter) +} + +// instrumentedRESTConfig creates a REST config with request instrumentation for monitoring. +// Adds HTTP transport wrapper for Prometheus metrics collection and request timeout configuration. +// +// Path Processing: Simplifies URL paths for metrics by taking the last segment, +// reducing cardinality of metric labels for better performance. +// +// Timeout: Applies the specified request timeout to prevent hanging requests. func instrumentedRESTConfig(kubeConfig, apiServerURL string, requestTimeout time.Duration) (*rest.Config, error) { config, err := GetRestConfig(kubeConfig, apiServerURL) if err != nil { @@ -443,8 +635,16 @@ func instrumentedRESTConfig(kubeConfig, apiServerURL string, requestTimeout time return config, nil } -// GetRestConfig returns the rest clients config to get automatically -// data if you run inside a cluster or by passing flags. +// GetRestConfig returns the REST client configuration for Kubernetes API access. +// Supports both in-cluster and external cluster configurations. +// +// Configuration Priority: +// 1. If kubeConfig is empty, tries the recommended home file (~/.kube/config) +// 2. If kubeConfig is still empty, uses in-cluster service account +// 3. Otherwise, uses the specified kubeConfig file +// +// API Server Override: The apiServerURL parameter can override the server URL +// from the kubeconfig file, useful for proxy scenarios or custom endpoints. func GetRestConfig(kubeConfig, apiServerURL string) (*rest.Config, error) { if kubeConfig == "" { if _, err := os.Stat(clientcmd.RecommendedHomeFile); err == nil { diff --git a/source/store_test.go b/source/store_test.go index c245b0efa..3b9c38498 100644 --- a/source/store_test.go +++ b/source/store_test.go @@ -27,6 +27,7 @@ import ( "github.com/stretchr/testify/suite" istioclient "istio.io/client-go/pkg/clientset/versioned" istiofake "istio.io/client-go/pkg/clientset/versioned/fake" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" @@ -239,3 +240,36 @@ func (suite *ByNamesTestSuite) TestDynamicKubernetesClientFails() { func TestByNames(t *testing.T) { suite.Run(t, new(ByNamesTestSuite)) } + +type minimalMockClientGenerator struct{} + +var errMock = errors.New("mock not implemented") + +func (m *minimalMockClientGenerator) KubeClient() (kubernetes.Interface, error) { return nil, errMock } +func (m *minimalMockClientGenerator) GatewayClient() (gateway.Interface, error) { return nil, errMock } +func (m *minimalMockClientGenerator) IstioClient() (istioclient.Interface, error) { + return nil, errMock +} +func (m *minimalMockClientGenerator) CloudFoundryClient(string, string, string) (*cfclient.Client, error) { + return nil, errMock +} +func (m *minimalMockClientGenerator) DynamicKubernetesClient() (dynamic.Interface, error) { + return nil, errMock +} +func (m *minimalMockClientGenerator) OpenShiftClient() (openshift.Interface, error) { + return nil, errMock +} + +func TestBuildWithConfig_InvalidSource(t *testing.T) { + ctx := context.Background() + p := &minimalMockClientGenerator{} + cfg := &Config{LabelFilter: labels.NewSelector()} + + src, err := BuildWithConfig(ctx, "not-a-source", p, cfg) + if src != nil { + t.Errorf("expected nil source for invalid type, got: %v", src) + } + if !errors.Is(err, ErrSourceNotFound) { + t.Errorf("expected ErrSourceNotFound, got: %v", err) + } +} From 495e2023a72626d0b9166b5110a90dae6b2361d0 Mon Sep 17 00:00:00 2001 From: schwajo <68690270+schwajo@users.noreply.github.com> Date: Mon, 30 Jun 2025 00:00:34 +0200 Subject: [PATCH 05/49] fix(rfc2136): use correct index for accessing UpdateOld (#5542) --- provider/rfc2136/rfc2136.go | 6 ++-- provider/rfc2136/rfc2136_test.go | 52 ++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/provider/rfc2136/rfc2136.go b/provider/rfc2136/rfc2136.go index d9f7196cf..c22dfa409 100644 --- a/provider/rfc2136/rfc2136.go +++ b/provider/rfc2136/rfc2136.go @@ -408,9 +408,11 @@ func (r *rfc2136Provider) ApplyChanges(ctx context.Context, changes *plan.Change zone := findMsgZone(ep, r.zoneNames) m[zone].SetUpdate(zone) - r.UpdateRecord(m[zone], changes.UpdateOld[i], ep) + // calculate corresponding index in the unsplitted UpdateOld for current endpoint ep in chunk + j := (c * r.batchChangeSize) + i + r.UpdateRecord(m[zone], changes.UpdateOld[j], ep) if r.createPTR && (ep.RecordType == "A" || ep.RecordType == "AAAA") { - r.RemoveReverseRecord(changes.UpdateOld[i].Targets[0], ep.DNSName) + r.RemoveReverseRecord(changes.UpdateOld[j].Targets[0], ep.DNSName) r.AddReverseRecord(ep.Targets[0], ep.DNSName) } } diff --git a/provider/rfc2136/rfc2136_test.go b/provider/rfc2136/rfc2136_test.go index 84178eea9..ed40d6150 100644 --- a/provider/rfc2136/rfc2136_test.go +++ b/provider/rfc2136/rfc2136_test.go @@ -256,6 +256,17 @@ func createRfc2136StubProviderWithStrategy(stub *rfc2136Stub, strategy string) ( return NewRfc2136Provider([]string{"rfc2136-host1", "rfc2136-host2", "rfc2136-host3"}, 0, nil, false, "key", "secret", "hmac-sha512", true, &endpoint.DomainFilter{}, false, 300*time.Second, false, false, "", "", "", 50, tlsConfig, strategy, stub) } +func createRfc2136StubProviderWithBatchChangeSize(stub *rfc2136Stub, batchChangeSize int) (provider.Provider, error) { + tlsConfig := TLSConfig{ + UseTLS: false, + SkipTLSVerify: false, + CAFilePath: "", + ClientCertFilePath: "", + ClientCertKeyFilePath: "", + } + return NewRfc2136Provider([]string{""}, 0, nil, false, "key", "secret", "hmac-sha512", true, &endpoint.DomainFilter{}, false, 300*time.Second, false, false, "", "", "", batchChangeSize, tlsConfig, "", stub) +} + func extractUpdateSectionFromMessage(msg fmt.Stringer) []string { const searchPattern = "UPDATE SECTION:" updateSectionOffset := strings.Index(msg.String(), searchPattern) @@ -959,3 +970,44 @@ func TestRandomLoadBalancing(t *testing.T) { assert.Greater(t, len(nameserverCounts), 1, "Expected multiple nameservers to be used in random strategy") } + +// TestRfc2136ApplyChangesWithMultipleChunks tests Updates with multiple chunks +func TestRfc2136ApplyChangesWithMultipleChunks(t *testing.T) { + stub := newStub() + + provider, err := createRfc2136StubProviderWithBatchChangeSize(stub, 2) + assert.NoError(t, err) + + var oldRecords []*endpoint.Endpoint + var newRecords []*endpoint.Endpoint + + for i := 1; i <= 4; i++ { + oldRecords = append(oldRecords, &endpoint.Endpoint{ + DNSName: fmt.Sprintf("%s%d%s", "v", i, ".foo.com"), + RecordType: "A", + Targets: []string{fmt.Sprintf("10.0.0.%d", i)}, + RecordTTL: endpoint.TTL(400), + }) + newRecords = append(newRecords, &endpoint.Endpoint{ + DNSName: fmt.Sprintf("%s%d%s", "v", i, ".foo.com"), + RecordType: "A", + Targets: []string{fmt.Sprintf("10.0.1.%d", i)}, + RecordTTL: endpoint.TTL(400), + }) + } + + p := &plan.Changes{ + UpdateOld: oldRecords, + UpdateNew: newRecords, + } + + err = provider.ApplyChanges(context.Background(), p) + assert.NoError(t, err) + + assert.Len(t, stub.updateMsgs, 4) + + assert.Contains(t, stub.updateMsgs[0].String(), "\nv1.foo.com.\t0\tNONE\tA\t10.0.0.1\nv1.foo.com.\t400\tIN\tA\t10.0.1.1\n") + assert.Contains(t, stub.updateMsgs[0].String(), "\nv2.foo.com.\t0\tNONE\tA\t10.0.0.2\nv2.foo.com.\t400\tIN\tA\t10.0.1.2\n") + assert.Contains(t, stub.updateMsgs[2].String(), "\nv3.foo.com.\t0\tNONE\tA\t10.0.0.3\nv3.foo.com.\t400\tIN\tA\t10.0.1.3\n") + assert.Contains(t, stub.updateMsgs[2].String(), "\nv4.foo.com.\t0\tNONE\tA\t10.0.0.4\nv4.foo.com.\t400\tIN\tA\t10.0.1.4\n") +} From 2b6c76e8824aa1405a890b39622c038f6a152768 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Jul 2025 02:17:25 -0700 Subject: [PATCH 06/49] chore(deps): bump the dev-dependencies group with 4 updates (#5593) Bumps the dev-dependencies group with 4 updates: [github.com/Yamashou/gqlgenc](https://github.com/Yamashou/gqlgenc), [github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue](https://github.com/aws/aws-sdk-go-v2), [github.com/aws/aws-sdk-go-v2/service/dynamodb](https://github.com/aws/aws-sdk-go-v2) and [github.com/linode/linodego](https://github.com/linode/linodego). Updates `github.com/Yamashou/gqlgenc` from 0.32.1 to 0.33.0 - [Release notes](https://github.com/Yamashou/gqlgenc/releases) - [Commits](https://github.com/Yamashou/gqlgenc/compare/v0.32.1...v0.33.0) Updates `github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue` from 1.19.3 to 1.19.4 - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/changelog-template.json) - [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/pi/v1.19.3...service/mq/v1.19.4) Updates `github.com/aws/aws-sdk-go-v2/service/dynamodb` from 1.43.4 to 1.44.0 - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/changelog-template.json) - [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/ivs/v1.43.4...service/s3/v1.44.0) Updates `github.com/linode/linodego` from 1.52.1 to 1.52.2 - [Release notes](https://github.com/linode/linodego/releases) - [Commits](https://github.com/linode/linodego/compare/v1.52.1...v1.52.2) --- updated-dependencies: - dependency-name: github.com/Yamashou/gqlgenc dependency-version: 0.33.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue dependency-version: 1.19.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: github.com/aws/aws-sdk-go-v2/service/dynamodb dependency-version: 1.44.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: github.com/linode/linodego dependency-version: 1.52.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dev-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 12 ++++++------ go.sum | 28 ++++++++++++++-------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/go.mod b/go.mod index 6f1eda1e6..4b52ed47d 100644 --- a/go.mod +++ b/go.mod @@ -9,15 +9,15 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 github.com/F5Networks/k8s-bigip-ctlr/v2 v2.20.0 - github.com/Yamashou/gqlgenc v0.32.1 + github.com/Yamashou/gqlgenc v0.33.0 github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 github.com/alecthomas/kingpin/v2 v2.4.0 github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 github.com/aws/aws-sdk-go-v2 v1.36.5 github.com/aws/aws-sdk-go-v2/config v1.29.17 github.com/aws/aws-sdk-go-v2/credentials v1.17.70 - github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.3 - github.com/aws/aws-sdk-go-v2/service/dynamodb v1.43.4 + github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.4 + github.com/aws/aws-sdk-go-v2/service/dynamodb v1.44.0 github.com/aws/aws-sdk-go-v2/service/route53 v1.52.2 github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.35.7 github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 @@ -38,7 +38,7 @@ require ( github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/linki/instrumented_http v0.3.0 - github.com/linode/linodego v1.52.1 + github.com/linode/linodego v1.52.2 github.com/maxatome/go-testdeep v1.14.0 github.com/miekg/dns v1.1.66 github.com/openshift/api v0.0.0-20230607130528-611114dca681 @@ -78,7 +78,7 @@ require ( cloud.google.com/go/auth v0.16.2 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f // indirect - github.com/99designs/gqlgen v0.17.71 // indirect + github.com/99designs/gqlgen v0.17.73 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect github.com/Masterminds/semver v1.5.0 // indirect @@ -162,7 +162,7 @@ require ( github.com/sosodev/duration v1.3.1 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/stretchr/objx v0.5.2 // indirect - github.com/vektah/gqlparser/v2 v2.5.25 // indirect + github.com/vektah/gqlparser/v2 v2.5.26 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect diff --git a/go.sum b/go.sum index 239bac249..d3454839b 100644 --- a/go.sum +++ b/go.sum @@ -12,8 +12,8 @@ code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f h1:UrKzEwTg code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f/go.mod h1:sk5LnIjB/nIEU7yP5sDQExVm62wu0pBh3yrElngUisI= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= git.lukeshu.com/go/libsystemd v0.5.3/go.mod h1:FfDoP0i92r4p5Vn4NCLxvjkd7rCOe6otPa4L6hZg9WM= -github.com/99designs/gqlgen v0.17.71 h1:6JdwweHlSMWGY+6VWY5ey0tO+sF8LckbUV0NmdOQi04= -github.com/99designs/gqlgen v0.17.71/go.mod h1:3yz6ekwCAjC90zaFvPoy+mEjaKiyYJjhtCnwn1seoxE= +github.com/99designs/gqlgen v0.17.73 h1:A3Ki+rHWqKbAOlg5fxiZBnz6OjW3nwupDHEG15gEsrg= +github.com/99designs/gqlgen v0.17.73/go.mod h1:2RyGWjy2k7W9jxrs8MOQthXGkD3L3oGr0jXW3Pu8lGg= github.com/Azure/azure-sdk-for-go v16.2.1+incompatible h1:KnPIugL51v3N3WwvaSmZbxukD1WuWXOiE9fRdu32f2I= github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U= @@ -76,8 +76,8 @@ github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:H github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= -github.com/Yamashou/gqlgenc v0.32.1 h1:EHs9//xQxXlyltkSFXM+fhO2rTXcWNw6FPKRJ6t+iQQ= -github.com/Yamashou/gqlgenc v0.32.1/go.mod h1:o5SxKt9d3+oUZ2i0V3CW8lHFyunfLR+KcKHubS4zf5E= +github.com/Yamashou/gqlgenc v0.33.0 h1:0fxTnNE8/JVmFpfo7reA5pEgOcr7VjNc+/nEpVhNjfc= +github.com/Yamashou/gqlgenc v0.33.0/go.mod h1:MZGXx/nALyxcehcFeLGmYiNsJ+hQTOGJzNYCGNX4rL0= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= @@ -120,8 +120,8 @@ github.com/aws/aws-sdk-go-v2/config v1.29.17 h1:jSuiQ5jEe4SAMH6lLRMY9OVC+TqJLP56 github.com/aws/aws-sdk-go-v2/config v1.29.17/go.mod h1:9P4wwACpbeXs9Pm9w1QTh6BwWwJjwYvJ1iCt5QbCXh8= github.com/aws/aws-sdk-go-v2/credentials v1.17.70 h1:ONnH5CM16RTXRkS8Z1qg7/s2eDOhHhaXVd72mmyv4/0= github.com/aws/aws-sdk-go-v2/credentials v1.17.70/go.mod h1:M+lWhhmomVGgtuPOhO85u4pEa3SmssPTdcYpP/5J/xc= -github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.3 h1:xQYRnbQ+ypDMCLiFlLw5cF7Xd6K+oaL7jco2zwIMqTs= -github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.3/go.mod h1:X7RC8FFkx0bjNJRBddd3xdoDaDmNLSxICFdIdJ7asqw= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.4 h1:jKR2jpZqpmBSAVX7xxdOi1E3Z0E9WizMIlxlGI3Hh9o= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.4/go.mod h1:ATyfcCpSMZuB/rnpFcVbiqrTiFzdwcTXeVbgEk6iXbY= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 h1:KAXP9JSHO1vKGCr5f4O6WmlVKLFFXgWYAGoJosorxzU= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32/go.mod h1:h4Sg6FQdexC1yYG9RDnOvLbW1a/P986++/Y/a+GyEM8= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 h1:SsytQyTMHMDPspp+spo7XwXTP44aJZZAC7fBV2C5+5s= @@ -130,8 +130,8 @@ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 h1:i2vNHQiXUvKhs3quBR github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36/go.mod h1:UdyGa7Q91id/sdyHPwth+043HhmP6yP9MBHgbZM0xo8= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= -github.com/aws/aws-sdk-go-v2/service/dynamodb v1.43.4 h1:Rv6o9v2AfdEIKoAa7pQpJ5ch9ji2HevFUvGY6ufawlI= -github.com/aws/aws-sdk-go-v2/service/dynamodb v1.43.4/go.mod h1:mWB0GE1bqcVSvpW7OtFA0sKuHk52+IqtnsYU2jUfYAs= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.44.0 h1:A99gjqZDbdhjtjJVZrmVzVKO2+p3MSg35bDWtbMQVxw= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.44.0/go.mod h1:mWB0GE1bqcVSvpW7OtFA0sKuHk52+IqtnsYU2jUfYAs= github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.25.6 h1:QHaS/SHXfyNycuu4GiWb+AfW5T3bput6X5E3Ai/Q31M= github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.25.6/go.mod h1:He/RikglWUczbkV+fkdpcV/3GdL/rTRNVy7VaUiezMo= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 h1:CXV68E2dNqhuynZJPB80bhPQwAKqBWVer887figW6Jc= @@ -676,8 +676,8 @@ github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-b github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/linki/instrumented_http v0.3.0 h1:dsN92+mXpfZtjJraartcQ99jnuw7fqsnPDjr85ma2dA= github.com/linki/instrumented_http v0.3.0/go.mod h1:pjYbItoegfuVi2GUOMhEqzvm/SJKuEL3H0tc8QRLRFk= -github.com/linode/linodego v1.52.1 h1:HJ1cz1n9n3chRP9UrtqmP91+xTi0Q5l+H/4z4tpkwgQ= -github.com/linode/linodego v1.52.1/go.mod h1:zEN2sX+cSdp67EuRY1HJiyuLujoa7HqvVwNEcJv3iXw= +github.com/linode/linodego v1.52.2 h1:N9ozU27To1LMSrDd8WvJZ5STSz1eGYdyLnxhAR/dIZg= +github.com/linode/linodego v1.52.2/go.mod h1:bI949fZaVchjWyKIA08hNyvAcV6BAS+PM2op3p7PAWA= github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= github.com/lyft/protoc-gen-star v0.4.10/go.mod h1:mE8fbna26u7aEA2QCVvvfBU/ZrPgocG1206xAFPcs94= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= @@ -907,8 +907,8 @@ github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE github.com/rogpeppe/go-internal v1.4.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rubenv/sql-migrate v0.0.0-20200212082348-64f95ea68aa3/go.mod h1:rtQlpHw+eR6UrqaS3kX1VYeaCxzCVdimDS7g5Ln4pPc= github.com/rubenv/sql-migrate v0.0.0-20200616145509-8d140a17f351/go.mod h1:DCgfY80j8GYL7MLEfvcpSFvjD0L5yZq/aZUJmhZklyg= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= @@ -1012,8 +1012,8 @@ github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtX github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= -github.com/vektah/gqlparser/v2 v2.5.25 h1:FmWtFEa+invTIzWlWK6Vk7BVEZU/97QBzeI8Z1JjGt8= -github.com/vektah/gqlparser/v2 v2.5.25/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= +github.com/vektah/gqlparser/v2 v2.5.26 h1:REqqFkO8+SOEgZHR/eHScjjVjGS8Nk3RMO/juiTobN4= +github.com/vektah/gqlparser/v2 v2.5.26/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= From d292de1dafaed2b95bd75c4fa60cd7afa8207ca5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Jul 2025 02:49:24 -0700 Subject: [PATCH 07/49] chore(deps): bump renovatebot/github-action (#5592) Bumps the dev-dependencies group with 1 update: [renovatebot/github-action](https://github.com/renovatebot/github-action). Updates `renovatebot/github-action` from 43.0.1 to 43.0.2 - [Release notes](https://github.com/renovatebot/github-action/releases) - [Changelog](https://github.com/renovatebot/github-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/renovatebot/github-action/compare/v43.0.1...v43.0.2) --- updated-dependencies: - dependency-name: renovatebot/github-action dependency-version: 43.0.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dev-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/dependency-update.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependency-update.yaml b/.github/workflows/dependency-update.yaml index 46de02a9e..550103407 100644 --- a/.github/workflows/dependency-update.yaml +++ b/.github/workflows/dependency-update.yaml @@ -17,7 +17,7 @@ jobs: uses: actions/checkout@v4.2.2 # https://github.com/renovatebot/github-action - name: self-hosted renovate - uses: renovatebot/github-action@v43.0.1 + uses: renovatebot/github-action@v43.0.2 with: # https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication token: ${{ secrets.GITHUB_TOKEN }} From 6e2fc4aa31bec2e1bc23b21d44e6832768f6932f Mon Sep 17 00:00:00 2001 From: Ivan Ka <5395690+ivankatliarchuk@users.noreply.github.com> Date: Wed, 2 Jul 2025 06:13:25 +0100 Subject: [PATCH 08/49] chore(codebase): enable linter nonamedreturns (#5594) * chore(codebase): enable linter nonamedreturns Signed-off-by: ivan katliarchuk * chore(codebase): enable linter nonamedreturns Signed-off-by: ivan katliarchuk --------- Signed-off-by: ivan katliarchuk --- .golangci.yml | 1 + endpoint/crypto.go | 6 +-- endpoint/crypto_test.go | 2 +- pkg/rfc2317/arpa.go | 2 +- provider/akamai/akamai.go | 5 +- provider/alibabacloud/alibaba_cloud.go | 54 +++++++++++---------- provider/alibabacloud/alibaba_cloud_test.go | 50 +++++++++---------- provider/aws/aws.go | 10 ++-- provider/awssd/aws_sd.go | 16 +++--- provider/azure/azure.go | 4 +- provider/azure/azure_private_dns.go | 5 +- provider/civo/civo_test.go | 2 +- provider/digitalocean/digital_ocean_test.go | 2 +- provider/dnsimple/dnsimple.go | 7 +-- provider/exoscale/exoscale.go | 5 +- provider/gandi/client.go | 16 +++--- provider/gandi/gandi.go | 6 +-- provider/gandi/gandi_test.go | 10 ++-- provider/godaddy/godaddy.go | 10 ++-- provider/google/google.go | 6 ++- provider/inmemory/inmemory.go | 2 +- provider/oci/oci_test.go | 42 ++++++++-------- provider/ovh/ovh.go | 2 +- provider/pdns/pdns.go | 53 +++++++++++--------- provider/plural/plural.go | 8 +-- provider/rfc2136/rfc2136.go | 10 ++-- provider/rfc2136/rfc2136_test.go | 2 +- provider/zonefinder.go | 6 ++- registry/txt.go | 6 +-- source/ambassador_host.go | 2 +- source/crd.go | 16 +++--- source/fqdn/fqdn.go | 2 +- source/istio_gateway.go | 8 +-- source/istio_virtualservice.go | 16 ++++-- source/service.go | 4 +- source/utils.go | 6 ++- 36 files changed, 218 insertions(+), 186 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index d03e3cda8..971c456f1 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -26,6 +26,7 @@ linters: - sloglint # Ensure consistent code style when using log/slog - asciicheck # Checks that all code identifiers does not have non-ASCII symbols in the name - nilnil # Checks that there is no simultaneous return of nil error and an nil value. ref: https://golangci-lint.run/usage/linters/#nilnil + - nonamedreturns # Checks that functions with named return values do not return named values. https://golangci-lint.run/usage/linters/#nonamedreturns - cyclop # Checks function and package cyclomatic complexity. https://golangci-lint.run/usage/linters/#cyclop # tests diff --git a/endpoint/crypto.go b/endpoint/crypto.go index 253cb227a..cd3fb2f6f 100644 --- a/endpoint/crypto.go +++ b/endpoint/crypto.go @@ -68,7 +68,7 @@ func EncryptText(text string, aesKey []byte, nonceEncoded []byte) (string, error // DecryptText decrypt gziped data using a supplied AES encryption key ang ungzip it // in case of decryption failed, will return original input and decryption error -func DecryptText(text string, aesKey []byte) (decryptResult string, encryptNonce string, err error) { +func DecryptText(text string, aesKey []byte) (string, string, error) { block, err := aes.NewCipher(aesKey) if err != nil { return "", "", err @@ -100,7 +100,7 @@ func DecryptText(text string, aesKey []byte) (decryptResult string, encryptNonce } // decompressData gzip compressed data -func decompressData(data []byte) (resData []byte, err error) { +func decompressData(data []byte) ([]byte, error) { gz, err := gzip.NewReader(bytes.NewBuffer(data)) if err != nil { return nil, err @@ -115,7 +115,7 @@ func decompressData(data []byte) (resData []byte, err error) { } // compressData by gzip, for minify data stored in registry -func compressData(data []byte) (compressedData []byte, err error) { +func compressData(data []byte) ([]byte, error) { var b bytes.Buffer gz, err := gzip.NewWriterLevel(&b, gzip.BestCompression) if err != nil { diff --git a/endpoint/crypto_test.go b/endpoint/crypto_test.go index fd910683d..4feeb073a 100644 --- a/endpoint/crypto_test.go +++ b/endpoint/crypto_test.go @@ -87,6 +87,6 @@ func TestGenerateNonceError(t *testing.T) { type faultyReader struct{} -func (f *faultyReader) Read(p []byte) (n int, err error) { +func (f *faultyReader) Read(p []byte) (int, error) { return 0, io.ErrUnexpectedEOF } diff --git a/pkg/rfc2317/arpa.go b/pkg/rfc2317/arpa.go index f11bac100..a79716bac 100644 --- a/pkg/rfc2317/arpa.go +++ b/pkg/rfc2317/arpa.go @@ -98,7 +98,7 @@ func CidrToInAddr(cidr string) (string, error) { // reverseaddr returns the in-addr.arpa. or ip6.arpa. hostname of the IP // address addr suitable for rDNS (PTR) record lookup or an error if it fails // to parse the IP address. -func reverseaddr(addr string) (arpa string, err error) { +func reverseaddr(addr string) (string, error) { ip := net.ParseIP(addr) if ip == nil { return "", &net.DNSError{Err: "unrecognized address", Name: addr} diff --git a/provider/akamai/akamai.go b/provider/akamai/akamai.go index 4066fbcc1..b513150bf 100644 --- a/provider/akamai/akamai.go +++ b/provider/akamai/akamai.go @@ -40,7 +40,7 @@ const ( maxInt = int(maxUint >> 1) ) -// edgeDNSClient is a proxy interface of the Akamai edgegrid configdns-v2 package that can be stubbed for testing. +// AkamaiDNSService is a proxy interface of the Akamai edgegrid configdns-v2 package that can be stubbed for testing. type AkamaiDNSService interface { ListZones(queryArgs dns.ZoneListQueryArgs) (*dns.ZoneListResponse, error) GetRecordsets(zone string, queryArgs dns.RecordsetQueryArgs) (*dns.RecordSetResponse, error) @@ -208,7 +208,8 @@ func (p AkamaiProvider) fetchZones() (akamaiZones, error) { } // Records returns the list of records in a given zone. -func (p AkamaiProvider) Records(context.Context) (endpoints []*endpoint.Endpoint, err error) { +func (p AkamaiProvider) Records(context.Context) ([]*endpoint.Endpoint, error) { + var endpoints []*endpoint.Endpoint zones, err := p.fetchZones() // returns a filtered set of zones if err != nil { log.Warnf("Failed to identify target zones! Error: %s", err.Error()) diff --git a/provider/alibabacloud/alibaba_cloud.go b/provider/alibabacloud/alibaba_cloud.go index 4cb41d1d1..fcb750f4f 100644 --- a/provider/alibabacloud/alibaba_cloud.go +++ b/provider/alibabacloud/alibaba_cloud.go @@ -29,7 +29,7 @@ import ( "github.com/aliyun/alibaba-cloud-sdk-go/services/alidns" "github.com/aliyun/alibaba-cloud-sdk-go/services/pvtz" "github.com/denverdino/aliyungo/metadata" - yaml "github.com/goccy/go-yaml" + "github.com/goccy/go-yaml" log "github.com/sirupsen/logrus" "sigs.k8s.io/external-dns/endpoint" @@ -49,22 +49,22 @@ const ( // AlibabaCloudDNSAPI is a minimal implementation of DNS API that we actually use, used primarily for unit testing. // See https://help.aliyun.com/document_detail/29739.html for descriptions of all of its methods. type AlibabaCloudDNSAPI interface { - AddDomainRecord(request *alidns.AddDomainRecordRequest) (response *alidns.AddDomainRecordResponse, err error) - DeleteDomainRecord(request *alidns.DeleteDomainRecordRequest) (response *alidns.DeleteDomainRecordResponse, err error) - UpdateDomainRecord(request *alidns.UpdateDomainRecordRequest) (response *alidns.UpdateDomainRecordResponse, err error) - DescribeDomainRecords(request *alidns.DescribeDomainRecordsRequest) (response *alidns.DescribeDomainRecordsResponse, err error) - DescribeDomains(request *alidns.DescribeDomainsRequest) (response *alidns.DescribeDomainsResponse, err error) + AddDomainRecord(request *alidns.AddDomainRecordRequest) (*alidns.AddDomainRecordResponse, error) + DeleteDomainRecord(request *alidns.DeleteDomainRecordRequest) (*alidns.DeleteDomainRecordResponse, error) + UpdateDomainRecord(request *alidns.UpdateDomainRecordRequest) (*alidns.UpdateDomainRecordResponse, error) + DescribeDomainRecords(request *alidns.DescribeDomainRecordsRequest) (*alidns.DescribeDomainRecordsResponse, error) + DescribeDomains(request *alidns.DescribeDomainsRequest) (*alidns.DescribeDomainsResponse, error) } // AlibabaCloudPrivateZoneAPI is a minimal implementation of Private Zone API that we actually use, used primarily for unit testing. // See https://help.aliyun.com/document_detail/66234.html for descriptions of all of its methods. type AlibabaCloudPrivateZoneAPI interface { - AddZoneRecord(request *pvtz.AddZoneRecordRequest) (response *pvtz.AddZoneRecordResponse, err error) - DeleteZoneRecord(request *pvtz.DeleteZoneRecordRequest) (response *pvtz.DeleteZoneRecordResponse, err error) - UpdateZoneRecord(request *pvtz.UpdateZoneRecordRequest) (response *pvtz.UpdateZoneRecordResponse, err error) - DescribeZoneRecords(request *pvtz.DescribeZoneRecordsRequest) (response *pvtz.DescribeZoneRecordsResponse, err error) - DescribeZones(request *pvtz.DescribeZonesRequest) (response *pvtz.DescribeZonesResponse, err error) - DescribeZoneInfo(request *pvtz.DescribeZoneInfoRequest) (response *pvtz.DescribeZoneInfoResponse, err error) + AddZoneRecord(request *pvtz.AddZoneRecordRequest) (*pvtz.AddZoneRecordResponse, error) + DeleteZoneRecord(request *pvtz.DeleteZoneRecordRequest) (*pvtz.DeleteZoneRecordResponse, error) + UpdateZoneRecord(request *pvtz.UpdateZoneRecordRequest) (*pvtz.UpdateZoneRecordResponse, error) + DescribeZoneRecords(request *pvtz.DescribeZoneRecordsRequest) (*pvtz.DescribeZoneRecordsResponse, error) + DescribeZones(request *pvtz.DescribeZonesRequest) (*pvtz.DescribeZonesResponse, error) + DescribeZoneInfo(request *pvtz.DescribeZoneInfoRequest) (*pvtz.DescribeZoneInfoResponse, error) } // AlibabaCloudProvider implements the DNS provider for Alibaba Cloud. @@ -284,19 +284,18 @@ func (p *AlibabaCloudProvider) refreshStsToken(sleepTime time.Duration) { // Records gets the current records. // // Returns the current records or an error if the operation failed. -func (p *AlibabaCloudProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, err error) { +func (p *AlibabaCloudProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { if p.privateZone { - endpoints, err = p.privateZoneRecords() + return p.privateZoneRecords() } else { - endpoints, err = p.recordsForDNS() + return p.recordsForDNS() } - return endpoints, err } // ApplyChanges applies the given changes. // // Returns nil if the operation was successful or an error if the operation failed. -func (p *AlibabaCloudProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { +func (p *AlibabaCloudProvider) ApplyChanges(_ context.Context, changes *plan.Changes) error { if changes == nil || len(changes.Create)+len(changes.Delete)+len(changes.UpdateNew) == 0 { // No op return nil @@ -318,11 +317,12 @@ func (p *AlibabaCloudProvider) getDNSName(rr, domain string) string { // recordsForDNS gets the current records. // // Returns the current records or an error if the operation failed. -func (p *AlibabaCloudProvider) recordsForDNS() (endpoints []*endpoint.Endpoint, _ error) { +func (p *AlibabaCloudProvider) recordsForDNS() ([]*endpoint.Endpoint, error) { records, err := p.records() if err != nil { return nil, err } + endpoints := make([]*endpoint.Endpoint, 0, len(records)) for _, recordList := range p.groupRecords(records) { name := p.getDNSName(recordList[0].RR, recordList[0].DomainName) recordType := recordList[0].Type @@ -360,8 +360,8 @@ func (p *AlibabaCloudProvider) getRecordKeyByEndpoint(endpoint *endpoint.Endpoin return endpoint.RecordType + ":" + endpoint.DNSName } -func (p *AlibabaCloudProvider) groupRecords(records []alidns.Record) (endpointMap map[string][]alidns.Record) { - endpointMap = make(map[string][]alidns.Record) +func (p *AlibabaCloudProvider) groupRecords(records []alidns.Record) map[string][]alidns.Record { + endpointMap := make(map[string][]alidns.Record) for _, record := range records { key := p.getRecordKey(record) @@ -675,7 +675,7 @@ func (p *AlibabaCloudProvider) updateRecords(recordMap map[string][]alidns.Recor return nil } -func (p *AlibabaCloudProvider) splitDNSName(dnsName string, hostedZoneDomains []string) (rr string, domain string) { +func (p *AlibabaCloudProvider) splitDNSName(dnsName string, hostedZoneDomains []string) (string, string) { name := strings.TrimSuffix(dnsName, ".") // sort zones by dot count; make sure subdomains sort earlier @@ -683,6 +683,8 @@ func (p *AlibabaCloudProvider) splitDNSName(dnsName string, hostedZoneDomains [] return strings.Count(hostedZoneDomains[i], ".") > strings.Count(hostedZoneDomains[j], ".") }) + var rr, domain string + for _, filter := range hostedZoneDomains { if strings.HasSuffix(name, "."+filter) { rr = name[0 : len(name)-len(filter)-1] @@ -819,8 +821,8 @@ func (p *AlibabaCloudProvider) getPrivateZones() (map[string]*alibabaPrivateZone return result, nil } -func (p *AlibabaCloudProvider) groupPrivateZoneRecords(zone *alibabaPrivateZone) (endpointMap map[string][]pvtz.Record) { - endpointMap = make(map[string][]pvtz.Record) +func (p *AlibabaCloudProvider) groupPrivateZoneRecords(zone *alibabaPrivateZone) map[string][]pvtz.Record { + endpointMap := make(map[string][]pvtz.Record) for _, record := range zone.records { key := record.Type + ":" + record.Rr @@ -834,12 +836,14 @@ func (p *AlibabaCloudProvider) groupPrivateZoneRecords(zone *alibabaPrivateZone) // recordsForPrivateZone gets the current records. // // Returns the current records or an error if the operation failed. -func (p *AlibabaCloudProvider) privateZoneRecords() (endpoints []*endpoint.Endpoint, _ error) { +func (p *AlibabaCloudProvider) privateZoneRecords() ([]*endpoint.Endpoint, error) { zones, err := p.getPrivateZones() if err != nil { return nil, err } + endpoints := make([]*endpoint.Endpoint, 0) + for _, zone := range zones { recordMap := p.groupPrivateZoneRecords(zone) for _, recordList := range recordMap { @@ -908,7 +912,7 @@ func (p *AlibabaCloudProvider) createPrivateZoneRecord(zones map[string]*alibaba func (p *AlibabaCloudProvider) createPrivateZoneRecords(zones map[string]*alibabaPrivateZone, endpoints []*endpoint.Endpoint) error { for _, endpoint := range endpoints { for _, target := range endpoint.Targets { - p.createPrivateZoneRecord(zones, endpoint, target) + _ = p.createPrivateZoneRecord(zones, endpoint, target) } } return nil diff --git a/provider/alibabacloud/alibaba_cloud_test.go b/provider/alibabacloud/alibaba_cloud_test.go index 2bf7392bc..9278e1514 100644 --- a/provider/alibabacloud/alibaba_cloud_test.go +++ b/provider/alibabacloud/alibaba_cloud_test.go @@ -55,7 +55,7 @@ func NewMockAlibabaCloudDNSAPI() *MockAlibabaCloudDNSAPI { return &api } -func (m *MockAlibabaCloudDNSAPI) AddDomainRecord(request *alidns.AddDomainRecordRequest) (response *alidns.AddDomainRecordResponse, err error) { +func (m *MockAlibabaCloudDNSAPI) AddDomainRecord(request *alidns.AddDomainRecordRequest) (*alidns.AddDomainRecordResponse, error) { ttl, _ := request.TTL.GetValue() m.records = append(m.records, alidns.Record{ RecordId: "3", @@ -65,11 +65,10 @@ func (m *MockAlibabaCloudDNSAPI) AddDomainRecord(request *alidns.AddDomainRecord RR: request.RR, Value: request.Value, }) - response = alidns.CreateAddDomainRecordResponse() - return response, nil + return alidns.CreateAddDomainRecordResponse(), nil } -func (m *MockAlibabaCloudDNSAPI) DeleteDomainRecord(request *alidns.DeleteDomainRecordRequest) (response *alidns.DeleteDomainRecordResponse, err error) { +func (m *MockAlibabaCloudDNSAPI) DeleteDomainRecord(request *alidns.DeleteDomainRecordRequest) (*alidns.DeleteDomainRecordResponse, error) { var result []alidns.Record for _, record := range m.records { if record.RecordId != request.RecordId { @@ -77,24 +76,24 @@ func (m *MockAlibabaCloudDNSAPI) DeleteDomainRecord(request *alidns.DeleteDomain } } m.records = result - response = alidns.CreateDeleteDomainRecordResponse() + response := alidns.CreateDeleteDomainRecordResponse() response.RecordId = request.RecordId return response, nil } -func (m *MockAlibabaCloudDNSAPI) UpdateDomainRecord(request *alidns.UpdateDomainRecordRequest) (response *alidns.UpdateDomainRecordResponse, err error) { +func (m *MockAlibabaCloudDNSAPI) UpdateDomainRecord(request *alidns.UpdateDomainRecordRequest) (*alidns.UpdateDomainRecordResponse, error) { ttl, _ := request.TTL.GetValue64() for i := range m.records { if m.records[i].RecordId == request.RecordId { m.records[i].TTL = ttl } } - response = alidns.CreateUpdateDomainRecordResponse() + response := alidns.CreateUpdateDomainRecordResponse() response.RecordId = request.RecordId return response, nil } -func (m *MockAlibabaCloudDNSAPI) DescribeDomains(request *alidns.DescribeDomainsRequest) (response *alidns.DescribeDomainsResponse, err error) { +func (m *MockAlibabaCloudDNSAPI) DescribeDomains(request *alidns.DescribeDomainsRequest) (*alidns.DescribeDomainsResponse, error) { var result alidns.DomainsInDescribeDomains for _, record := range m.records { domain := alidns.Domain{} @@ -103,19 +102,19 @@ func (m *MockAlibabaCloudDNSAPI) DescribeDomains(request *alidns.DescribeDomains DomainName: domain.DomainName, }) } - response = alidns.CreateDescribeDomainsResponse() + response := alidns.CreateDescribeDomainsResponse() response.Domains = result return response, nil } -func (m *MockAlibabaCloudDNSAPI) DescribeDomainRecords(request *alidns.DescribeDomainRecordsRequest) (response *alidns.DescribeDomainRecordsResponse, err error) { +func (m *MockAlibabaCloudDNSAPI) DescribeDomainRecords(request *alidns.DescribeDomainRecordsRequest) (*alidns.DescribeDomainRecordsResponse, error) { var result []alidns.Record for _, record := range m.records { if record.DomainName == request.DomainName { result = append(result, record) } } - response = alidns.CreateDescribeDomainRecordsResponse() + response := alidns.CreateDescribeDomainRecordsResponse() response.DomainRecords.Record = result return response, nil } @@ -158,7 +157,7 @@ func NewMockAlibabaCloudPrivateZoneAPI() *MockAlibabaCloudPrivateZoneAPI { return &api } -func (m *MockAlibabaCloudPrivateZoneAPI) AddZoneRecord(request *pvtz.AddZoneRecordRequest) (response *pvtz.AddZoneRecordResponse, err error) { +func (m *MockAlibabaCloudPrivateZoneAPI) AddZoneRecord(request *pvtz.AddZoneRecordRequest) (*pvtz.AddZoneRecordResponse, error) { ttl, _ := request.Ttl.GetValue() m.records = append(m.records, pvtz.Record{ RecordId: 3, @@ -167,11 +166,10 @@ func (m *MockAlibabaCloudPrivateZoneAPI) AddZoneRecord(request *pvtz.AddZoneReco Rr: request.Rr, Value: request.Value, }) - response = pvtz.CreateAddZoneRecordResponse() - return response, nil + return pvtz.CreateAddZoneRecordResponse(), nil } -func (m *MockAlibabaCloudPrivateZoneAPI) DeleteZoneRecord(request *pvtz.DeleteZoneRecordRequest) (response *pvtz.DeleteZoneRecordResponse, err error) { +func (m *MockAlibabaCloudPrivateZoneAPI) DeleteZoneRecord(request *pvtz.DeleteZoneRecordRequest) (*pvtz.DeleteZoneRecordResponse, error) { recordID, _ := request.RecordId.GetValue64() var result []pvtz.Record @@ -181,11 +179,10 @@ func (m *MockAlibabaCloudPrivateZoneAPI) DeleteZoneRecord(request *pvtz.DeleteZo } } m.records = result - response = pvtz.CreateDeleteZoneRecordResponse() - return response, nil + return pvtz.CreateDeleteZoneRecordResponse(), nil } -func (m *MockAlibabaCloudPrivateZoneAPI) UpdateZoneRecord(request *pvtz.UpdateZoneRecordRequest) (response *pvtz.UpdateZoneRecordResponse, err error) { +func (m *MockAlibabaCloudPrivateZoneAPI) UpdateZoneRecord(request *pvtz.UpdateZoneRecordRequest) (*pvtz.UpdateZoneRecordResponse, error) { recordID, _ := request.RecordId.GetValue64() ttl, _ := request.Ttl.GetValue() for i := range m.records { @@ -193,24 +190,23 @@ func (m *MockAlibabaCloudPrivateZoneAPI) UpdateZoneRecord(request *pvtz.UpdateZo m.records[i].Ttl = ttl } } - response = pvtz.CreateUpdateZoneRecordResponse() - return response, nil + return pvtz.CreateUpdateZoneRecordResponse(), nil } -func (m *MockAlibabaCloudPrivateZoneAPI) DescribeZoneRecords(request *pvtz.DescribeZoneRecordsRequest) (response *pvtz.DescribeZoneRecordsResponse, err error) { - response = pvtz.CreateDescribeZoneRecordsResponse() +func (m *MockAlibabaCloudPrivateZoneAPI) DescribeZoneRecords(request *pvtz.DescribeZoneRecordsRequest) (*pvtz.DescribeZoneRecordsResponse, error) { + response := pvtz.CreateDescribeZoneRecordsResponse() response.Records.Record = append(response.Records.Record, m.records...) return response, nil } -func (m *MockAlibabaCloudPrivateZoneAPI) DescribeZones(request *pvtz.DescribeZonesRequest) (response *pvtz.DescribeZonesResponse, err error) { - response = pvtz.CreateDescribeZonesResponse() +func (m *MockAlibabaCloudPrivateZoneAPI) DescribeZones(_ *pvtz.DescribeZonesRequest) (*pvtz.DescribeZonesResponse, error) { + response := pvtz.CreateDescribeZonesResponse() response.Zones.Zone = append(response.Zones.Zone, m.zone) return response, nil } -func (m *MockAlibabaCloudPrivateZoneAPI) DescribeZoneInfo(request *pvtz.DescribeZoneInfoRequest) (response *pvtz.DescribeZoneInfoResponse, err error) { - response = pvtz.CreateDescribeZoneInfoResponse() +func (m *MockAlibabaCloudPrivateZoneAPI) DescribeZoneInfo(_ *pvtz.DescribeZoneInfoRequest) (*pvtz.DescribeZoneInfoResponse, error) { + response := pvtz.CreateDescribeZoneInfoResponse() response.ZoneId = m.zone.ZoneId response.ZoneName = m.zone.ZoneName response.BindVpcs = pvtz.BindVpcsInDescribeZoneInfo{Vpc: make([]pvtz.VpcInDescribeZoneInfo, len(m.zone.Vpcs.Vpc))} @@ -336,7 +332,7 @@ func TestAlibabaCloudProvider_ApplyChanges_HaveNoDefinedZoneDomain(t *testing.T) changes := plan.Changes{ Create: []*endpoint.Endpoint{ { - DNSName: "www.example.com", //no found this zone by API: DescribeDomains + DNSName: "www.example.com", // no found this zone by API: DescribeDomains RecordType: "A", RecordTTL: 300, Targets: endpoint.NewTargets("9.9.9.9"), diff --git a/provider/aws/aws.go b/provider/aws/aws.go index 9e896235c..458b33089 100644 --- a/provider/aws/aws.go +++ b/provider/aws/aws.go @@ -454,10 +454,10 @@ func containsOctalSequence(domain string) bool { } // Records returns the list of records in a given hosted zone. -func (p *AWSProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, _ error) { +func (p *AWSProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { zones, err := p.zones(ctx) if err != nil { - return nil, provider.NewSoftErrorf("records retrieval failed: %w", err) + return nil, provider.NewSoftErrorf("records retrieval failed: %v", err) } return p.records(ctx, zones) @@ -940,11 +940,13 @@ func (p *AWSProvider) newChange(action route53types.ChangeAction, ep *endpoint.E } // searches for `changes` that are contained in `queue` and returns the `changes` separated by whether they were found in the queue (`foundChanges`) or not (`notFoundChanges`) -func findChangesInQueue(changes Route53Changes, queue Route53Changes) (foundChanges, notFoundChanges Route53Changes) { +func findChangesInQueue(changes Route53Changes, queue Route53Changes) (Route53Changes, Route53Changes) { if queue == nil { return Route53Changes{}, changes } + var foundChanges, notFoundChanges Route53Changes + for _, c := range changes { found := false for _, qc := range queue { @@ -959,7 +961,7 @@ func findChangesInQueue(changes Route53Changes, queue Route53Changes) (foundChan } } - return + return foundChanges, notFoundChanges } // group the given changes by name and ownership relation to ensure these are always submitted in the same transaction to Route53; diff --git a/provider/awssd/aws_sd.go b/provider/awssd/aws_sd.go index f64e88728..510fee810 100644 --- a/provider/awssd/aws_sd.go +++ b/provider/awssd/aws_sd.go @@ -130,12 +130,14 @@ func awsTags(tags map[string]string) []sdtypes.Tag { } // Records returns list of all endpoints. -func (p *AWSSDProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, err error) { +func (p *AWSSDProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { namespaces, err := p.ListNamespaces(ctx) if err != nil { return nil, err } + endpoints := make([]*endpoint.Endpoint, 0) + for _, ns := range namespaces { services, err := p.ListServicesByNamespaceID(ctx, ns.Id) if err != nil { @@ -244,12 +246,14 @@ func (p *AWSSDProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) return nil } -func (p *AWSSDProvider) updatesToCreates(changes *plan.Changes) (creates []*endpoint.Endpoint, deletes []*endpoint.Endpoint) { +func (p *AWSSDProvider) updatesToCreates(changes *plan.Changes) ([]*endpoint.Endpoint, []*endpoint.Endpoint) { updateNewMap := map[string]*endpoint.Endpoint{} for _, e := range changes.UpdateNew { updateNewMap[e.DNSName] = e } + var creates, deletes []*endpoint.Endpoint + for _, old := range changes.UpdateOld { current := updateNewMap[old.DNSName] @@ -618,12 +622,10 @@ func matchingNamespaces(hostname string, namespaces []*sdtypes.NamespaceSummary) return matchingNamespaces } -// parse hostname to namespace (domain) and service -func (p *AWSSDProvider) parseHostname(hostname string) (namespace string, service string) { +// parseHostname parse hostname to namespace (domain) and service +func (p *AWSSDProvider) parseHostname(hostname string) (string, string) { parts := strings.Split(hostname, ".") - service = parts[0] - namespace = strings.Join(parts[1:], ".") - return + return strings.Join(parts[1:], "."), parts[0] } // determine service routing policy based on endpoint type diff --git a/provider/azure/azure.go b/provider/azure/azure.go index 438035e95..d53efa32f 100644 --- a/provider/azure/azure.go +++ b/provider/azure/azure.go @@ -106,12 +106,14 @@ func NewAzureProvider(configFile string, domainFilter *endpoint.DomainFilter, zo // Records gets the current records. // // Returns the current records or an error if the operation failed. -func (p *AzureProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, _ error) { +func (p *AzureProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { zones, err := p.zones(ctx) if err != nil { return nil, err } + endpoints := make([]*endpoint.Endpoint, 0) + for _, zone := range zones { pager := p.recordSetsClient.NewListAllByDNSZonePager(p.resourceGroup, *zone.Name, &dns.RecordSetsClientListAllByDNSZoneOptions{Top: nil}) for pager.More() { diff --git a/provider/azure/azure_private_dns.go b/provider/azure/azure_private_dns.go index 108600556..2e0fefd33 100644 --- a/provider/azure/azure_private_dns.go +++ b/provider/azure/azure_private_dns.go @@ -101,7 +101,7 @@ func NewAzurePrivateDNSProvider(configFile string, domainFilter *endpoint.Domain // Records gets the current records. // // Returns the current records or an error if the operation failed. -func (p *AzurePrivateDNSProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, _ error) { +func (p *AzurePrivateDNSProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { zones, err := p.zones(ctx) if err != nil { return nil, err @@ -109,12 +109,13 @@ func (p *AzurePrivateDNSProvider) Records(ctx context.Context) (endpoints []*end log.Debugf("Retrieving Azure Private DNS Records for resource group '%s'", p.resourceGroup) + endpoints := make([]*endpoint.Endpoint, 0) for _, zone := range zones { pager := p.recordSetsClient.NewListPager(p.resourceGroup, *zone.Name, &privatedns.RecordSetsClientListOptions{Top: nil}) for pager.More() { nextResult, err := pager.NextPage(ctx) if err != nil { - return nil, provider.NewSoftError(fmt.Errorf("failed to fetch dns records: %w", err)) + return nil, provider.NewSoftErrorf("failed to fetch dns records: %v", err) } for _, recordSet := range nextResult.Value { diff --git a/provider/civo/civo_test.go b/provider/civo/civo_test.go index 27020a274..80910887d 100644 --- a/provider/civo/civo_test.go +++ b/provider/civo/civo_test.go @@ -1169,7 +1169,7 @@ func TestCivoChangesEmpty(t *testing.T) { // This function is an adapted copy of the testify package's ElementsMatch function with the // call to ObjectsAreEqual replaced with cmp.Equal which better handles struct's with pointers to // other structs. It also ignores ordering when comparing unlike cmp.Equal. -func elementsMatch(t *testing.T, listA, listB interface{}, msgAndArgs ...interface{}) (ok bool) { +func elementsMatch(t *testing.T, listA, listB interface{}, msgAndArgs ...interface{}) bool { if listA == nil && listB == nil { return true } else if listA == nil { diff --git a/provider/digitalocean/digital_ocean_test.go b/provider/digitalocean/digital_ocean_test.go index 18f62aeb4..e174fe067 100644 --- a/provider/digitalocean/digital_ocean_test.go +++ b/provider/digitalocean/digital_ocean_test.go @@ -194,7 +194,7 @@ func isEmpty(xs interface{}) bool { // This function is an adapted copy of the testify package's ElementsMatch function with the // call to ObjectsAreEqual replaced with cmp.Equal which better handles struct's with pointers to // other structs. It also ignores ordering when comparing unlike cmp.Equal. -func elementsMatch(t *testing.T, listA, listB interface{}, msgAndArgs ...interface{}) (ok bool) { +func elementsMatch(t *testing.T, listA, listB interface{}, msgAndArgs ...interface{}) bool { if listA == nil && listB == nil { return true } else if listA == nil { diff --git a/provider/dnsimple/dnsimple.go b/provider/dnsimple/dnsimple.go index 5a0ee365b..5d120e67a 100644 --- a/provider/dnsimple/dnsimple.go +++ b/provider/dnsimple/dnsimple.go @@ -130,7 +130,7 @@ func NewDnsimpleProvider(domainFilter *endpoint.DomainFilter, zoneIDFilter provi } // GetAccountID returns the account ID given DNSimple credentials. -func (p *dnsimpleProvider) GetAccountID(ctx context.Context) (accountID string, err error) { +func (p *dnsimpleProvider) GetAccountID(ctx context.Context) (string, error) { // get DNSimple client accountID whoamiResponse, err := p.identity.Whoami(ctx) if err != nil { @@ -191,11 +191,12 @@ func (p *dnsimpleProvider) Zones(ctx context.Context) (map[string]dnsimple.Zone, } // Records returns a list of endpoints in a given zone -func (p *dnsimpleProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, _ error) { +func (p *dnsimpleProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { zones, err := p.Zones(ctx) if err != nil { return nil, err } + endpoints := make([]*endpoint.Endpoint, 0) for _, zone := range zones { page := 1 listOptions := &dnsimple.ZoneRecordListOptions{} @@ -318,7 +319,7 @@ func (p *dnsimpleProvider) submitChanges(ctx context.Context, changes []*dnsimpl } // GetRecordID returns the record ID for a given record name and zone. -func (p *dnsimpleProvider) GetRecordID(ctx context.Context, zone string, recordName string) (recordID int64, err error) { +func (p *dnsimpleProvider) GetRecordID(ctx context.Context, zone string, recordName string) (int64, error) { page := 1 listOptions := &dnsimple.ZoneRecordListOptions{Name: &recordName} for { diff --git a/provider/exoscale/exoscale.go b/provider/exoscale/exoscale.go index 195d7cca0..86c40388b 100644 --- a/provider/exoscale/exoscale.go +++ b/provider/exoscale/exoscale.go @@ -292,9 +292,8 @@ func (f *zoneFilter) Zones(zones map[string]string) map[string]string { // EndpointZoneID determines zoneID for endpoint from map[zoneID]zoneName by taking longest suffix zoneName match in endpoint DNSName // returns empty string if no matches are found -func (f *zoneFilter) EndpointZoneID(endpoint *endpoint.Endpoint, zones map[string]string) (zoneID string, name string) { - var matchZoneID string - var matchZoneName string +func (f *zoneFilter) EndpointZoneID(endpoint *endpoint.Endpoint, zones map[string]string) (string, string) { + var matchZoneID, matchZoneName, name string for zoneID, zoneName := range zones { if strings.HasSuffix(endpoint.DNSName, "."+zoneName) && len(zoneName) > len(matchZoneName) { matchZoneName = zoneName diff --git a/provider/gandi/client.go b/provider/gandi/client.go index a5e55334f..049018343 100644 --- a/provider/gandi/client.go +++ b/provider/gandi/client.go @@ -19,14 +19,14 @@ import ( ) type DomainClientAdapter interface { - ListDomains() (domains []domain.ListResponse, err error) + ListDomains() ([]domain.ListResponse, error) } type domainClient struct { Client *domain.Domain } -func (p *domainClient) ListDomains() (domains []domain.ListResponse, err error) { +func (p *domainClient) ListDomains() ([]domain.ListResponse, error) { return p.Client.ListDomains() } @@ -54,9 +54,9 @@ type standardError struct { type LiveDNSClientAdapter interface { GetDomainRecords(fqdn string) (records []livedns.DomainRecord, err error) - CreateDomainRecord(fqdn, name, recordtype string, ttl int, values []string) (response standardResponse, err error) + CreateDomainRecord(fqdn, name, recordtype string, ttl int, values []string) (standardResponse, error) DeleteDomainRecord(fqdn, name, recordtype string) (err error) - UpdateDomainRecordByNameAndType(fqdn, name, recordtype string, ttl int, values []string) (response standardResponse, err error) + UpdateDomainRecordByNameAndType(fqdn, name, recordtype string, ttl int, values []string) (standardResponse, error) } type LiveDNSClient struct { @@ -67,11 +67,11 @@ func NewLiveDNSClient(client *livedns.LiveDNS) LiveDNSClientAdapter { return &LiveDNSClient{client} } -func (p *LiveDNSClient) GetDomainRecords(fqdn string) (records []livedns.DomainRecord, err error) { +func (p *LiveDNSClient) GetDomainRecords(fqdn string) ([]livedns.DomainRecord, error) { return p.Client.GetDomainRecords(fqdn) } -func (p *LiveDNSClient) CreateDomainRecord(fqdn, name, recordtype string, ttl int, values []string) (response standardResponse, err error) { +func (p *LiveDNSClient) CreateDomainRecord(fqdn, name, recordtype string, ttl int, values []string) (standardResponse, error) { res, err := p.Client.CreateDomainRecord(fqdn, name, recordtype, ttl, values) if err != nil { return standardResponse{}, err @@ -93,11 +93,11 @@ func (p *LiveDNSClient) CreateDomainRecord(fqdn, name, recordtype string, ttl in }, err } -func (p *LiveDNSClient) DeleteDomainRecord(fqdn, name, recordtype string) (err error) { +func (p *LiveDNSClient) DeleteDomainRecord(fqdn, name, recordtype string) error { return p.Client.DeleteDomainRecord(fqdn, name, recordtype) } -func (p *LiveDNSClient) UpdateDomainRecordByNameAndType(fqdn, name, recordtype string, ttl int, values []string) (response standardResponse, err error) { +func (p *LiveDNSClient) UpdateDomainRecordByNameAndType(fqdn, name, recordtype string, ttl int, values []string) (standardResponse, error) { res, err := p.Client.UpdateDomainRecordByNameAndType(fqdn, name, recordtype, ttl, values) if err != nil { return standardResponse{}, err diff --git a/provider/gandi/gandi.go b/provider/gandi/gandi.go index fd7234157..e8fdea337 100644 --- a/provider/gandi/gandi.go +++ b/provider/gandi/gandi.go @@ -83,12 +83,12 @@ func NewGandiProvider(ctx context.Context, domainFilter *endpoint.DomainFilter, return gandiProvider, nil } -func (p *GandiProvider) Zones() (zones []string, err error) { +func (p *GandiProvider) Zones() ([]string, error) { availableDomains, err := p.DomainClient.ListDomains() if err != nil { return nil, err } - zones = []string{} + zones := []string{} for _, domain := range availableDomains { if !p.domainFilter.Match(domain.FQDN) { log.Debugf("Excluding domain %s by domain-filter", domain.FQDN) @@ -156,7 +156,7 @@ func (p *GandiProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) return p.submitChanges(ctx, combinedChanges) } -func (p *GandiProvider) submitChanges(ctx context.Context, changes []*GandiChanges) error { +func (p *GandiProvider) submitChanges(_ context.Context, changes []*GandiChanges) error { if len(changes) == 0 { log.Infof("All records are already up to date") return nil diff --git a/provider/gandi/gandi_test.go b/provider/gandi/gandi_test.go index 72c73aeb4..8e564b8e1 100644 --- a/provider/gandi/gandi_test.go +++ b/provider/gandi/gandi_test.go @@ -49,7 +49,7 @@ const ( // Mock all methods -func (m *mockGandiClient) GetDomainRecords(fqdn string) (records []livedns.DomainRecord, err error) { +func (m *mockGandiClient) GetDomainRecords(fqdn string) ([]livedns.DomainRecord, error) { m.Actions = append(m.Actions, MockAction{ Name: "GetDomainRecords", FQDN: fqdn, @@ -62,7 +62,7 @@ func (m *mockGandiClient) GetDomainRecords(fqdn string) (records []livedns.Domai return m.RecordsToReturn, nil } -func (m *mockGandiClient) CreateDomainRecord(fqdn, name, recordtype string, ttl int, values []string) (response standardResponse, err error) { +func (m *mockGandiClient) CreateDomainRecord(fqdn, name, recordtype string, ttl int, values []string) (standardResponse, error) { m.Actions = append(m.Actions, MockAction{ Name: "CreateDomainRecord", FQDN: fqdn, @@ -81,7 +81,7 @@ func (m *mockGandiClient) CreateDomainRecord(fqdn, name, recordtype string, ttl return standardResponse{}, nil } -func (m *mockGandiClient) DeleteDomainRecord(fqdn, name, recordtype string) (err error) { +func (m *mockGandiClient) DeleteDomainRecord(fqdn, name, recordtype string) error { m.Actions = append(m.Actions, MockAction{ Name: "DeleteDomainRecord", FQDN: fqdn, @@ -98,7 +98,7 @@ func (m *mockGandiClient) DeleteDomainRecord(fqdn, name, recordtype string) (err return nil } -func (m *mockGandiClient) UpdateDomainRecordByNameAndType(fqdn, name, recordtype string, ttl int, values []string) (response standardResponse, err error) { +func (m *mockGandiClient) UpdateDomainRecordByNameAndType(fqdn, name, recordtype string, ttl int, values []string) (standardResponse, error) { m.Actions = append(m.Actions, MockAction{ Name: "UpdateDomainRecordByNameAndType", FQDN: fqdn, @@ -117,7 +117,7 @@ func (m *mockGandiClient) UpdateDomainRecordByNameAndType(fqdn, name, recordtype return standardResponse{}, nil } -func (m *mockGandiClient) ListDomains() (domains []domain.ListResponse, err error) { +func (m *mockGandiClient) ListDomains() ([]domain.ListResponse, error) { m.Actions = append(m.Actions, MockAction{ Name: "ListDomains", }) diff --git a/provider/godaddy/godaddy.go b/provider/godaddy/godaddy.go index 3b318824e..045cd2332 100644 --- a/provider/godaddy/godaddy.go +++ b/provider/godaddy/godaddy.go @@ -121,7 +121,9 @@ func (z gdZoneIDName) add(zoneID string, zoneRecord *gdRecords) { z[zoneID] = zoneRecord } -func (z gdZoneIDName) findZoneRecord(hostname string) (suitableZoneID string, suitableZoneRecord *gdRecords) { +func (z gdZoneIDName) findZoneRecord(hostname string) (string, *gdRecords) { + var suitableZoneID string + var suitableZoneRecord *gdRecords for zoneID, zoneRecord := range z { if hostname == zoneRecord.zone || strings.HasSuffix(hostname, "."+zoneRecord.zone) { if suitableZoneRecord == nil || len(zoneRecord.zone) > len(suitableZoneRecord.zone) { @@ -131,11 +133,11 @@ func (z gdZoneIDName) findZoneRecord(hostname string) (suitableZoneID string, su } } - return + return suitableZoneID, suitableZoneRecord } // NewGoDaddyProvider initializes a new GoDaddy DNS based Provider. -func NewGoDaddyProvider(ctx context.Context, domainFilter *endpoint.DomainFilter, ttl int64, apiKey, apiSecret string, useOTE, dryRun bool) (*GDProvider, error) { +func NewGoDaddyProvider(_ context.Context, domainFilter *endpoint.DomainFilter, ttl int64, apiKey, apiSecret string, useOTE, dryRun bool) (*GDProvider, error) { client, err := NewClient(useOTE, apiKey, apiSecret) if err != nil { return nil, err @@ -218,7 +220,7 @@ func (p *GDProvider) zonesRecords(ctx context.Context, all bool) ([]string, []gd return zones, allRecords, nil } -func (p *GDProvider) records(ctx *context.Context, zone string, all bool) (*gdRecords, error) { +func (p *GDProvider) records(_ *context.Context, zone string, all bool) (*gdRecords, error) { var recordsIds []gdRecordField log.Debugf("GoDaddy: Getting records for %s", zone) diff --git a/provider/google/google.go b/provider/google/google.go index f2897d9f5..68217e37b 100644 --- a/provider/google/google.go +++ b/provider/google/google.go @@ -207,12 +207,14 @@ func (p *GoogleProvider) Zones(ctx context.Context) (map[string]*dns.ManagedZone } // Records returns the list of records in all relevant zones. -func (p *GoogleProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, _ error) { +func (p *GoogleProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { zones, err := p.Zones(ctx) if err != nil { return nil, err } + endpoints := make([]*endpoint.Endpoint, 0) + f := func(resp *dns.ResourceRecordSetsListResponse) error { for _, r := range resp.Rrsets { if !p.SupportedRecordType(r.Type) { @@ -226,7 +228,7 @@ func (p *GoogleProvider) Records(ctx context.Context) (endpoints []*endpoint.End for _, z := range zones { if err := p.resourceRecordSetsClient.List(p.project, z.Name).Pages(ctx, f); err != nil { - return nil, provider.NewSoftError(fmt.Errorf("failed to list records in zone %s: %w", z.Name, err)) + return nil, provider.NewSoftErrorf("failed to list records in zone %s: %v", z.Name, err) } } diff --git a/provider/inmemory/inmemory.go b/provider/inmemory/inmemory.go index e1d01baff..b17ef900e 100644 --- a/provider/inmemory/inmemory.go +++ b/provider/inmemory/inmemory.go @@ -230,7 +230,7 @@ func (f *filter) Zones(zones map[string]string) map[string]string { // EndpointZoneID determines zoneID for endpoint from map[zoneID]zoneName by taking longest suffix zoneName match in endpoint DNSName // returns empty string if no match found -func (f *filter) EndpointZoneID(endpoint *endpoint.Endpoint, zones map[string]string) (zoneID string) { +func (f *filter) EndpointZoneID(endpoint *endpoint.Endpoint, zones map[string]string) string { var matchZoneID, matchZoneName string for zoneID, zoneName := range zones { if strings.HasSuffix(endpoint.DNSName, zoneName) && len(zoneName) > len(matchZoneName) { diff --git a/provider/oci/oci_test.go b/provider/oci/oci_test.go index aeb27b8fb..a7c914cd8 100644 --- a/provider/oci/oci_test.go +++ b/provider/oci/oci_test.go @@ -70,7 +70,7 @@ func buildZoneResponseItems(scope dns.ListZonesScopeEnum, privateZones, globalZo } } -func (c *mockOCIDNSClient) ListZones(_ context.Context, request dns.ListZonesRequest) (response dns.ListZonesResponse, err error) { +func (c *mockOCIDNSClient) ListZones(_ context.Context, request dns.ListZonesRequest) (dns.ListZonesResponse, error) { if request.Page == nil || *request.Page == "0" { return dns.ListZonesResponse{ Items: buildZoneResponseItems(request.Scope, []dns.ZoneSummary{testPrivateZoneSummaryBaz}, []dns.ZoneSummary{testGlobalZoneSummaryFoo}), @@ -82,9 +82,11 @@ func (c *mockOCIDNSClient) ListZones(_ context.Context, request dns.ListZonesReq }, nil } -func (c *mockOCIDNSClient) GetZoneRecords(ctx context.Context, request dns.GetZoneRecordsRequest) (response dns.GetZoneRecordsResponse, err error) { +func (c *mockOCIDNSClient) GetZoneRecords(ctx context.Context, request dns.GetZoneRecordsRequest) (dns.GetZoneRecordsResponse, error) { + var response dns.GetZoneRecordsResponse + var err error if request.ZoneNameOrId == nil { - return + return response, err } switch *request.ZoneNameOrId { @@ -120,12 +122,11 @@ func (c *mockOCIDNSClient) GetZoneRecords(ctx context.Context, request dns.GetZo }} } } - - return + return response, err } -func (c *mockOCIDNSClient) PatchZoneRecords(ctx context.Context, request dns.PatchZoneRecordsRequest) (response dns.PatchZoneRecordsResponse, err error) { - return // Provider does not use the response so nothing to do here. +func (c *mockOCIDNSClient) PatchZoneRecords(_ context.Context, request dns.PatchZoneRecordsRequest) (dns.PatchZoneRecordsResponse, error) { + return dns.PatchZoneRecordsResponse{}, nil } // newOCIProvider creates an OCI provider with API calls mocked out. @@ -549,7 +550,7 @@ func newMutableMockOCIDNSClient(zones []dns.ZoneSummary, recordsByZone map[strin return c } -func (c *mutableMockOCIDNSClient) ListZones(ctx context.Context, request dns.ListZonesRequest) (response dns.ListZonesResponse, err error) { +func (c *mutableMockOCIDNSClient) ListZones(_ context.Context, _ dns.ListZonesRequest) (dns.ListZonesResponse, error) { var zones []dns.ZoneSummary for _, v := range c.zones { zones = append(zones, v) @@ -557,16 +558,15 @@ func (c *mutableMockOCIDNSClient) ListZones(ctx context.Context, request dns.Lis return dns.ListZonesResponse{Items: zones}, nil } -func (c *mutableMockOCIDNSClient) GetZoneRecords(ctx context.Context, request dns.GetZoneRecordsRequest) (response dns.GetZoneRecordsResponse, err error) { +func (c *mutableMockOCIDNSClient) GetZoneRecords(_ context.Context, request dns.GetZoneRecordsRequest) (dns.GetZoneRecordsResponse, error) { + var response dns.GetZoneRecordsResponse if request.ZoneNameOrId == nil { - err = errors.New("no name or id") - return + return response, errors.New("no name or id") } records, ok := c.records[*request.ZoneNameOrId] if !ok { - err = errors.New("zone not found") - return + return response, errors.New("zone not found") } var items []dns.Record @@ -575,7 +575,7 @@ func (c *mutableMockOCIDNSClient) GetZoneRecords(ctx context.Context, request dn } response.Items = items - return + return response, nil } func ociRecordKey(rType, domain string, ip string) string { @@ -592,16 +592,15 @@ func sortEndpointTargets(endpoints []*endpoint.Endpoint) { } } -func (c *mutableMockOCIDNSClient) PatchZoneRecords(ctx context.Context, request dns.PatchZoneRecordsRequest) (response dns.PatchZoneRecordsResponse, err error) { +func (c *mutableMockOCIDNSClient) PatchZoneRecords(ctx context.Context, request dns.PatchZoneRecordsRequest) (dns.PatchZoneRecordsResponse, error) { + var response dns.PatchZoneRecordsResponse if request.ZoneNameOrId == nil { - err = errors.New("no name or id") - return + return response, errors.New("no name or id") } records, ok := c.records[*request.ZoneNameOrId] if !ok { - err = errors.New("zone not found") - return + return response, errors.New("zone not found") } // Ensure that ADD operations occur after REMOVE. @@ -622,11 +621,10 @@ func (c *mutableMockOCIDNSClient) PatchZoneRecords(ctx context.Context, request case dns.RecordOperationOperationRemove: delete(records, k) default: - err = fmt.Errorf("unsupported operation %q", op.Operation) - return + return response, fmt.Errorf("unsupported operation %q", op.Operation) } } - return + return response, nil } // TestMutableMockOCIDNSClient exists because one must always test one's tests diff --git a/provider/ovh/ovh.go b/provider/ovh/ovh.go index 48d3125f8..d3c696ad6 100644 --- a/provider/ovh/ovh.go +++ b/provider/ovh/ovh.go @@ -242,7 +242,7 @@ func (p *OVHProvider) handleSingleZoneUpdate(ctx context.Context, zoneName strin } // ApplyChanges applies a given set of changes in a given zone. -func (p *OVHProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) (err error) { +func (p *OVHProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { zones, records := p.lastRunZones, p.lastRunRecords defer func() { p.lastRunRecords = []ovhRecord{} diff --git a/provider/pdns/pdns.go b/provider/pdns/pdns.go index f1916f817..748e220ed 100644 --- a/provider/pdns/pdns.go +++ b/provider/pdns/pdns.go @@ -114,15 +114,14 @@ func (tlsConfig *TLSConfig) setHTTPClient(pdnsClientConfig *pgo.Configuration) e } // Function for debug printing -func stringifyHTTPResponseBody(r *http.Response) (body string) { +func stringifyHTTPResponseBody(r *http.Response) string { if r == nil { return "" } buf := new(bytes.Buffer) - buf.ReadFrom(r.Body) - body = buf.String() - return body + _, _ = buf.ReadFrom(r.Body) + return buf.String() } // PDNSAPIProvider : Interface used and extended by the PDNSAPIClient struct as @@ -145,7 +144,10 @@ type PDNSAPIClient struct { // ListZones : Method returns all enabled zones from PowerDNS // ref: https://doc.powerdns.com/authoritative/http-api/zone.html#get--servers-server_id-zones -func (c *PDNSAPIClient) ListZones() (zones []pgo.Zone, resp *http.Response, err error) { +func (c *PDNSAPIClient) ListZones() ([]pgo.Zone, *http.Response, error) { + var zones []pgo.Zone + var resp *http.Response + var err error for i := 0; i < retryLimit; i++ { zones, resp, err = c.client.ZonesApi.ListZones(c.authCtx, c.serverID) if err != nil { @@ -157,11 +159,14 @@ func (c *PDNSAPIClient) ListZones() (zones []pgo.Zone, resp *http.Response, err return zones, resp, err } - return zones, resp, provider.NewSoftError(fmt.Errorf("unable to list zones: %w", err)) + return zones, resp, provider.NewSoftErrorf("unable to list zones: %v", err) } // PartitionZones : Method returns a slice of zones that adhere to the domain filter and a slice of ones that does not adhere to the filter -func (c *PDNSAPIClient) PartitionZones(zones []pgo.Zone) (filteredZones []pgo.Zone, residualZones []pgo.Zone) { +func (c *PDNSAPIClient) PartitionZones(zones []pgo.Zone) ([]pgo.Zone, []pgo.Zone) { + var filteredZones []pgo.Zone + var residualZones []pgo.Zone + if c.domainFilter.IsConfigured() { for _, zone := range zones { if c.domainFilter.Match(zone.Name) { @@ -178,9 +183,9 @@ func (c *PDNSAPIClient) PartitionZones(zones []pgo.Zone) (filteredZones []pgo.Zo // ListZone : Method returns the details of a specific zone from PowerDNS // ref: https://doc.powerdns.com/authoritative/http-api/zone.html#get--servers-server_id-zones-zone_id -func (c *PDNSAPIClient) ListZone(zoneID string) (zone pgo.Zone, resp *http.Response, err error) { +func (c *PDNSAPIClient) ListZone(zoneID string) (pgo.Zone, *http.Response, error) { for i := 0; i < retryLimit; i++ { - zone, resp, err = c.client.ZonesApi.ListZone(c.authCtx, c.serverID, zoneID) + zone, resp, err := c.client.ZonesApi.ListZone(c.authCtx, c.serverID, zoneID) if err != nil { log.Debugf("Unable to fetch zone %v", err) log.Debugf("Retrying ListZone() ... %d", i) @@ -190,12 +195,14 @@ func (c *PDNSAPIClient) ListZone(zoneID string) (zone pgo.Zone, resp *http.Respo return zone, resp, err } - return zone, resp, provider.NewSoftError(fmt.Errorf("unable to list zone: %w", err)) + return pgo.Zone{}, nil, provider.NewSoftErrorf("unable to list zone") } // PatchZone : Method used to update the contents of a particular zone from PowerDNS // ref: https://doc.powerdns.com/authoritative/http-api/zone.html#patch--servers-server_id-zones-zone_id -func (c *PDNSAPIClient) PatchZone(zoneID string, zoneStruct pgo.Zone) (resp *http.Response, err error) { +func (c *PDNSAPIClient) PatchZone(zoneID string, zoneStruct pgo.Zone) (*http.Response, error) { + var resp *http.Response + var err error for i := 0; i < retryLimit; i++ { resp, err = c.client.ZonesApi.PatchZone(c.authCtx, c.serverID, zoneID, zoneStruct) if err != nil { @@ -207,7 +214,7 @@ func (c *PDNSAPIClient) PatchZone(zoneID string, zoneStruct pgo.Zone) (resp *htt return resp, err } - return resp, provider.NewSoftError(fmt.Errorf("unable to patch zone: %w", err)) + return resp, provider.NewSoftErrorf("unable to patch zone: %v", err) } // PDNSProvider is an implementation of the Provider interface for PowerDNS @@ -252,9 +259,9 @@ func NewPDNSProvider(ctx context.Context, config PDNSConfig) (*PDNSProvider, err return provider, nil } -func (p *PDNSProvider) convertRRSetToEndpoints(rr pgo.RrSet) (endpoints []*endpoint.Endpoint, _ error) { - endpoints = []*endpoint.Endpoint{} - targets := []string{} +func (p *PDNSProvider) convertRRSetToEndpoints(rr pgo.RrSet) ([]*endpoint.Endpoint, error) { + endpoints := make([]*endpoint.Endpoint, 0) + targets := make([]string, 0) rrType_ := rr.Type_ for _, record := range rr.Records { @@ -271,8 +278,8 @@ func (p *PDNSProvider) convertRRSetToEndpoints(rr pgo.RrSet) (endpoints []*endpo } // ConvertEndpointsToZones marshals endpoints into pdns compatible Zone structs -func (p *PDNSProvider) ConvertEndpointsToZones(eps []*endpoint.Endpoint, changetype pdnsChangeType) (zonelist []pgo.Zone, _ error) { - zonelist = []pgo.Zone{} +func (p *PDNSProvider) ConvertEndpointsToZones(eps []*endpoint.Endpoint, changetype pdnsChangeType) ([]pgo.Zone, error) { + var zoneList = make([]pgo.Zone, 0) endpoints := make([]*endpoint.Endpoint, len(eps)) copy(endpoints, eps) @@ -354,7 +361,7 @@ func (p *PDNSProvider) ConvertEndpointsToZones(eps []*endpoint.Endpoint, changet } } if len(zone.Rrsets) > 0 { - zonelist = append(zonelist, zone) + zoneList = append(zoneList, zone) } } @@ -379,9 +386,9 @@ func (p *PDNSProvider) ConvertEndpointsToZones(eps []*endpoint.Endpoint, changet log.Warnf("No matching zones were found for the following endpoints: %+v", endpoints) } - log.Debugf("Zone List generated from Endpoints: %+v", zonelist) + log.Debugf("Zone List generated from Endpoints: %+v", zoneList) - return zonelist, nil + return zoneList, nil } // mutateRecords takes a list of endpoints and creates, replaces or deletes them based on the changetype @@ -407,17 +414,19 @@ func (p *PDNSProvider) mutateRecords(endpoints []*endpoint.Endpoint, changetype } // Records returns all DNS records controlled by the configured PDNS server (for all zones) -func (p *PDNSProvider) Records(ctx context.Context) (endpoints []*endpoint.Endpoint, _ error) { +func (p *PDNSProvider) Records(_ context.Context) ([]*endpoint.Endpoint, error) { zones, _, err := p.client.ListZones() if err != nil { return nil, err } filteredZones, _ := p.client.PartitionZones(zones) + var endpoints []*endpoint.Endpoint + for _, zone := range filteredZones { z, _, err := p.client.ListZone(zone.Id) if err != nil { - return nil, provider.NewSoftError(fmt.Errorf("unable to fetch records: %w", err)) + return nil, provider.NewSoftErrorf("unable to fetch records: %v", err) } for _, rr := range z.Rrsets { diff --git a/provider/plural/plural.go b/provider/plural/plural.go index 691c7c683..bdb2e19c3 100644 --- a/provider/plural/plural.go +++ b/provider/plural/plural.go @@ -66,17 +66,17 @@ func NewPluralProvider(cluster, provider string) (*PluralProvider, error) { }, nil } -func (p *PluralProvider) Records(_ context.Context) (endpoints []*endpoint.Endpoint, err error) { +func (p *PluralProvider) Records(_ context.Context) ([]*endpoint.Endpoint, error) { records, err := p.Client.DnsRecords() if err != nil { - return + return nil, err } - endpoints = make([]*endpoint.Endpoint, len(records)) + endpoints := make([]*endpoint.Endpoint, len(records)) for i, record := range records { endpoints[i] = endpoint.NewEndpoint(record.Name, record.Type, record.Records...) } - return + return endpoints, nil } func (p *PluralProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) ([]*endpoint.Endpoint, error) { diff --git a/provider/rfc2136/rfc2136.go b/provider/rfc2136/rfc2136.go index c22dfa409..816b782ca 100644 --- a/provider/rfc2136/rfc2136.go +++ b/provider/rfc2136/rfc2136.go @@ -169,13 +169,13 @@ func NewRfc2136Provider(hosts []string, port int, zoneNames []string, insecure b } // KeyData will return TKEY name and TSIG handle to use for followon actions with a secure connection -func (r *rfc2136Provider) KeyData(nameserver string) (keyName string, handle *gss.Client, err error) { - handle, err = gss.NewClient(new(dns.Client)) +func (r *rfc2136Provider) KeyData(nameserver string) (string, *gss.Client, error) { + handle, err := gss.NewClient(new(dns.Client)) if err != nil { - return keyName, handle, err + return "", handle, err } - keyName, _, err = handle.NegotiateContextWithCredentials(nameserver, r.krb5Realm, r.krb5Username, r.krb5Password) + keyName, _, err := handle.NegotiateContextWithCredentials(nameserver, r.krb5Realm, r.krb5Username, r.krb5Password) if err != nil { return keyName, handle, err } @@ -247,7 +247,7 @@ OuterLoop: return eps, nil } -func (r *rfc2136Provider) IncomeTransfer(m *dns.Msg, nameserver string) (env chan *dns.Envelope, err error) { +func (r *rfc2136Provider) IncomeTransfer(m *dns.Msg, nameserver string) (chan *dns.Envelope, error) { t := new(dns.Transfer) if !r.insecure && !r.gssTsig { t.TsigSecret = map[string]string{r.tsigKeyName: r.tsigSecret} diff --git a/provider/rfc2136/rfc2136_test.go b/provider/rfc2136/rfc2136_test.go index ed40d6150..fc3d65c3f 100644 --- a/provider/rfc2136/rfc2136_test.go +++ b/provider/rfc2136/rfc2136_test.go @@ -149,7 +149,7 @@ func (r *rfc2136Stub) setOutput(output []string) error { return nil } -func (r *rfc2136Stub) IncomeTransfer(m *dns.Msg, a string) (env chan *dns.Envelope, err error) { +func (r *rfc2136Stub) IncomeTransfer(m *dns.Msg, a string) (chan *dns.Envelope, error) { outChan := make(chan *dns.Envelope) go func() { for _, e := range r.output { diff --git a/provider/zonefinder.go b/provider/zonefinder.go index a47060bc4..08e6b3162 100644 --- a/provider/zonefinder.go +++ b/provider/zonefinder.go @@ -41,7 +41,7 @@ func (z ZoneIDName) Add(zoneID, zoneName string) { // SRV records as per RFC 2782, or TXT record for services) that are not // IDNA-aware and cannot represent non-ASCII labels. Skipping these labels // ensures compatibility with such use cases. -func (z ZoneIDName) FindZone(hostname string) (suitableZoneID, suitableZoneName string) { +func (z ZoneIDName) FindZone(hostname string) (string, string) { var name string domainLabels := strings.Split(hostname, ".") for i, label := range domainLabels { @@ -57,6 +57,8 @@ func (z ZoneIDName) FindZone(hostname string) (suitableZoneID, suitableZoneName } name = strings.Join(domainLabels, ".") + var suitableZoneID, suitableZoneName string + for zoneID, zoneName := range z { if name == zoneName || strings.HasSuffix(name, "."+zoneName) { if suitableZoneName == "" || len(zoneName) > len(suitableZoneName) { @@ -65,5 +67,5 @@ func (z ZoneIDName) FindZone(hostname string) (suitableZoneID, suitableZoneName } } } - return + return suitableZoneID, suitableZoneName } diff --git a/registry/txt.go b/registry/txt.go index 6075f1afe..325996936 100644 --- a/registry/txt.go +++ b/registry/txt.go @@ -338,7 +338,7 @@ func newaffixNameMapper(prefix, suffix, wildcardReplacement string) affixNameMap // extractRecordTypeDefaultPosition extracts record type from the default position // when not using '%{record_type}' in the prefix/suffix -func extractRecordTypeDefaultPosition(name string) (baseName, recordType string) { +func extractRecordTypeDefaultPosition(name string) (string, string) { nameS := strings.Split(name, "-") for _, t := range getSupportedTypes() { if nameS[0] == strings.ToLower(t) { @@ -350,7 +350,7 @@ func extractRecordTypeDefaultPosition(name string) (baseName, recordType string) // dropAffixExtractType strips TXT record to find an endpoint name it manages // it also returns the record type -func (pr affixNameMapper) dropAffixExtractType(name string) (baseName, recordType string) { +func (pr affixNameMapper) dropAffixExtractType(name string) (string, string) { prefix := pr.prefix suffix := pr.suffix @@ -397,7 +397,7 @@ func (pr affixNameMapper) isSuffix() bool { return len(pr.prefix) == 0 && len(pr.suffix) > 0 } -func (pr affixNameMapper) toEndpointName(txtDNSName string) (endpointName string, recordType string) { +func (pr affixNameMapper) toEndpointName(txtDNSName string) (string, string) { lowerDNSName := strings.ToLower(txtDNSName) // drop prefix diff --git a/source/ambassador_host.go b/source/ambassador_host.go index 3de5d5bbe..d126c2941 100644 --- a/source/ambassador_host.go +++ b/source/ambassador_host.go @@ -228,7 +228,7 @@ func (sc *ambassadorHostSource) targetsFromAmbassadorLoadBalancer(ctx context.Co // // Returns namespace, name, error. -func parseAmbLoadBalancerService(service string) (namespace, name string, err error) { +func parseAmbLoadBalancerService(service string) (string, string, error) { // Start by assuming that we have namespace/name. parts := strings.Split(service, "/") diff --git a/source/crd.go b/source/crd.go index a0c54e1e0..011915a37 100644 --- a/source/crd.go +++ b/source/crd.go @@ -125,7 +125,7 @@ func NewCRDSource(crdClient rest.Interface, namespace, kind string, annotationFi // missed or dropped events are handled. specify resync period 0 to avoid unnecessary sync handler invocations. informer := cache.NewSharedInformer( &cache.ListWatch{ - ListWithContextFunc: func(ctx context.Context, lo metav1.ListOptions) (result runtime.Object, err error) { + ListWithContextFunc: func(ctx context.Context, lo metav1.ListOptions) (runtime.Object, error) { return sourceCrd.List(ctx, &lo) }, WatchFuncWithContext: func(ctx context.Context, lo metav1.ListOptions) (watch.Interface, error) { @@ -235,20 +235,19 @@ func (cs *crdSource) watch(ctx context.Context, opts *metav1.ListOptions) (watch Watch(ctx) } -func (cs *crdSource) List(ctx context.Context, opts *metav1.ListOptions) (result *apiv1alpha1.DNSEndpointList, err error) { - result = &apiv1alpha1.DNSEndpointList{} - err = cs.crdClient.Get(). +func (cs *crdSource) List(ctx context.Context, opts *metav1.ListOptions) (*apiv1alpha1.DNSEndpointList, error) { + result := &apiv1alpha1.DNSEndpointList{} + return result, cs.crdClient.Get(). Namespace(cs.namespace). Resource(cs.crdResource). VersionedParams(opts, cs.codec). Do(ctx). Into(result) - return } -func (cs *crdSource) UpdateStatus(ctx context.Context, dnsEndpoint *apiv1alpha1.DNSEndpoint) (result *apiv1alpha1.DNSEndpoint, err error) { - result = &apiv1alpha1.DNSEndpoint{} - err = cs.crdClient.Put(). +func (cs *crdSource) UpdateStatus(ctx context.Context, dnsEndpoint *apiv1alpha1.DNSEndpoint) (*apiv1alpha1.DNSEndpoint, error) { + result := &apiv1alpha1.DNSEndpoint{} + return result, cs.crdClient.Put(). Namespace(dnsEndpoint.Namespace). Resource(cs.crdResource). Name(dnsEndpoint.Name). @@ -256,7 +255,6 @@ func (cs *crdSource) UpdateStatus(ctx context.Context, dnsEndpoint *apiv1alpha1. Body(dnsEndpoint). Do(ctx). Into(result) - return } // filterByAnnotations filters a list of dnsendpoints by a given annotation selector. diff --git a/source/fqdn/fqdn.go b/source/fqdn/fqdn.go index 1c01d0450..17e91d80a 100644 --- a/source/fqdn/fqdn.go +++ b/source/fqdn/fqdn.go @@ -28,7 +28,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) -func ParseTemplate(input string) (tmpl *template.Template, err error) { +func ParseTemplate(input string) (*template.Template, error) { if input == "" { return nil, nil } diff --git a/source/istio_gateway.go b/source/istio_gateway.go index 39d96d6b3..60b3d70a3 100644 --- a/source/istio_gateway.go +++ b/source/istio_gateway.go @@ -225,7 +225,7 @@ func (sc *gatewaySource) filterByAnnotations(gateways []*networkingv1alpha3.Gate return filteredList, nil } -func (sc *gatewaySource) targetsFromIngress(ctx context.Context, ingressStr string, gateway *networkingv1alpha3.Gateway) (targets endpoint.Targets, err error) { +func (sc *gatewaySource) targetsFromIngress(ctx context.Context, ingressStr string, gateway *networkingv1alpha3.Gateway) (endpoint.Targets, error) { namespace, name, err := ParseIngress(ingressStr) if err != nil { return nil, fmt.Errorf("failed to parse Ingress annotation on Gateway (%s/%s): %w", gateway.Namespace, gateway.Name, err) @@ -234,10 +234,12 @@ func (sc *gatewaySource) targetsFromIngress(ctx context.Context, ingressStr stri namespace = gateway.Namespace } + targets := make(endpoint.Targets, 0) + ingress, err := sc.kubeClient.NetworkingV1().Ingresses(namespace).Get(ctx, name, metav1.GetOptions{}) if err != nil { log.Error(err) - return + return nil, err } for _, lb := range ingress.Status.LoadBalancer.Ingress { if lb.IP != "" { @@ -246,7 +248,7 @@ func (sc *gatewaySource) targetsFromIngress(ctx context.Context, ingressStr stri targets = append(targets, lb.Hostname) } } - return + return targets, nil } func (sc *gatewaySource) targetsFromGateway(ctx context.Context, gateway *networkingv1alpha3.Gateway) (endpoint.Targets, error) { diff --git a/source/istio_virtualservice.go b/source/istio_virtualservice.go index cd106b2f3..2f9aaf439 100644 --- a/source/istio_virtualservice.go +++ b/source/istio_virtualservice.go @@ -416,7 +416,10 @@ func virtualServiceBindsToGateway(virtualService *networkingv1alpha3.VirtualServ return false } -func parseGateway(gateway string) (namespace, name string, err error) { +// TODO: similar to ParseIngress +func parseGateway(gateway string) (string, string, error) { + var namespace, name string + var err error parts := strings.Split(gateway, "/") if len(parts) == 2 { namespace, name = parts[0], parts[1] @@ -426,10 +429,10 @@ func parseGateway(gateway string) (namespace, name string, err error) { err = fmt.Errorf("invalid gateway name (name or namespace/name) found '%v'", gateway) } - return + return namespace, name, err } -func (sc *virtualServiceSource) targetsFromIngress(ctx context.Context, ingressStr string, gateway *networkingv1alpha3.Gateway) (targets endpoint.Targets, err error) { +func (sc *virtualServiceSource) targetsFromIngress(ctx context.Context, ingressStr string, gateway *networkingv1alpha3.Gateway) (endpoint.Targets, error) { namespace, name, err := ParseIngress(ingressStr) if err != nil { return nil, fmt.Errorf("failed to parse Ingress annotation on Gateway (%s/%s): %w", gateway.Namespace, gateway.Name, err) @@ -441,8 +444,11 @@ func (sc *virtualServiceSource) targetsFromIngress(ctx context.Context, ingressS ingress, err := sc.kubeClient.NetworkingV1().Ingresses(namespace).Get(ctx, name, metav1.GetOptions{}) if err != nil { log.Error(err) - return + return nil, err } + + targets := make(endpoint.Targets, 0) + for _, lb := range ingress.Status.LoadBalancer.Ingress { if lb.IP != "" { targets = append(targets, lb.IP) @@ -450,7 +456,7 @@ func (sc *virtualServiceSource) targetsFromIngress(ctx context.Context, ingressS targets = append(targets, lb.Hostname) } } - return + return targets, nil } func (sc *virtualServiceSource) targetsFromGateway(ctx context.Context, gateway *networkingv1alpha3.Gateway) (endpoint.Targets, error) { diff --git a/source/service.go b/source/service.go index 942558552..2f96b94d3 100644 --- a/source/service.go +++ b/source/service.go @@ -527,7 +527,7 @@ func (sc *serviceSource) filterByServiceType(services []*v1.Service) []*v1.Servi return result } -func (sc *serviceSource) generateEndpoints(svc *v1.Service, hostname string, providerSpecific endpoint.ProviderSpecific, setIdentifier string, useClusterIP bool) (endpoints []*endpoint.Endpoint) { +func (sc *serviceSource) generateEndpoints(svc *v1.Service, hostname string, providerSpecific endpoint.ProviderSpecific, setIdentifier string, useClusterIP bool) []*endpoint.Endpoint { hostname = strings.TrimSuffix(hostname, ".") resource := fmt.Sprintf("service/%s/%s", svc.Namespace, svc.Name) @@ -536,6 +536,8 @@ func (sc *serviceSource) generateEndpoints(svc *v1.Service, hostname string, pro targets := annotations.TargetsFromTargetAnnotation(svc.Annotations) + endpoints := make([]*endpoint.Endpoint, 0) + if len(targets) == 0 { switch svc.Spec.Type { case v1.ServiceTypeLoadBalancer: diff --git a/source/utils.go b/source/utils.go index 1023c3442..5858fddb8 100644 --- a/source/utils.go +++ b/source/utils.go @@ -42,7 +42,9 @@ func suitableType(target string) string { // ParseIngress parses an ingress string in the format "namespace/name" or "name". // It returns the namespace and name extracted from the string, or an error if the format is invalid. // If the namespace is not provided, it defaults to an empty string. -func ParseIngress(ingress string) (namespace, name string, err error) { +func ParseIngress(ingress string) (string, string, error) { + var namespace, name string + var err error parts := strings.Split(ingress, "/") if len(parts) == 2 { namespace, name = parts[0], parts[1] @@ -52,7 +54,7 @@ func ParseIngress(ingress string) (namespace, name string, err error) { err = fmt.Errorf("invalid ingress name (name or namespace/name) found %q", ingress) } - return + return namespace, name, err } // MatchesServiceSelector checks if all key-value pairs in the selector map From 3f43531d23469404f2a8283e869be1e8adc20942 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Jul 2025 22:53:25 -0700 Subject: [PATCH 09/49] chore(deps): bump github.com/oracle/oci-go-sdk/v65 (#5597) Bumps the dev-dependencies group with 1 update: [github.com/oracle/oci-go-sdk/v65](https://github.com/oracle/oci-go-sdk). Updates `github.com/oracle/oci-go-sdk/v65` from 65.94.0 to 65.95.0 - [Release notes](https://github.com/oracle/oci-go-sdk/releases) - [Changelog](https://github.com/oracle/oci-go-sdk/blob/master/CHANGELOG.md) - [Commits](https://github.com/oracle/oci-go-sdk/compare/v65.94.0...v65.95.0) --- updated-dependencies: - dependency-name: github.com/oracle/oci-go-sdk/v65 dependency-version: 65.95.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dev-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 4b52ed47d..c5fea239b 100644 --- a/go.mod +++ b/go.mod @@ -43,7 +43,7 @@ require ( github.com/miekg/dns v1.1.66 github.com/openshift/api v0.0.0-20230607130528-611114dca681 github.com/openshift/client-go v0.0.0-20230607134213-3cd0021bbee3 - github.com/oracle/oci-go-sdk/v65 v65.94.0 + github.com/oracle/oci-go-sdk/v65 v65.95.0 github.com/ovh/go-ovh v1.9.0 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pluralsh/gqlclient v1.12.2 diff --git a/go.sum b/go.sum index d3454839b..90ef1452f 100644 --- a/go.sum +++ b/go.sum @@ -819,8 +819,8 @@ github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxS github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= -github.com/oracle/oci-go-sdk/v65 v65.94.0 h1:6Vbv7oCb8plv7wNnx0cI+6kBQ7RUpZAvj3tQaHDXULo= -github.com/oracle/oci-go-sdk/v65 v65.94.0/go.mod h1:u6XRPsw9tPziBh76K7GrrRXPa8P8W3BQeqJ6ZZt9VLA= +github.com/oracle/oci-go-sdk/v65 v65.95.0 h1:fI+/mfJOS2DkQ+/AFSyJAfn1XFR4TTGm2AhN6xbsi00= +github.com/oracle/oci-go-sdk/v65 v65.95.0/go.mod h1:u6XRPsw9tPziBh76K7GrrRXPa8P8W3BQeqJ6ZZt9VLA= github.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE= github.com/ovh/go-ovh v1.9.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c= github.com/oxtoacart/bpool v0.0.0-20150712133111-4e1c5567d7c2 h1:CXwSGu/LYmbjEab5aMCs5usQRVBGThelUKBNnoSOuso= From 2e50ddb72a7cbc60ff9b7a4c425c43bc3d06cef9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Jul 2025 23:51:26 -0700 Subject: [PATCH 10/49] chore(deps): bump google.golang.org/api in the dev-dependencies group (#5605) Bumps the dev-dependencies group with 1 update: [google.golang.org/api](https://github.com/googleapis/google-api-go-client). Updates `google.golang.org/api` from 0.239.0 to 0.240.0 - [Release notes](https://github.com/googleapis/google-api-go-client/releases) - [Changelog](https://github.com/googleapis/google-api-go-client/blob/main/CHANGES.md) - [Commits](https://github.com/googleapis/google-api-go-client/compare/v0.239.0...v0.240.0) --- updated-dependencies: - dependency-name: google.golang.org/api dependency-version: 0.240.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dev-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index c5fea239b..c18f444cb 100644 --- a/go.mod +++ b/go.mod @@ -62,7 +62,7 @@ require ( golang.org/x/sync v0.15.0 golang.org/x/text v0.26.0 golang.org/x/time v0.12.0 - google.golang.org/api v0.239.0 + google.golang.org/api v0.240.0 gopkg.in/ns1/ns1-go.v2 v2.14.4 istio.io/api v1.26.2 istio.io/client-go v1.26.2 diff --git a/go.sum b/go.sum index 90ef1452f..a75ee74e6 100644 --- a/go.sum +++ b/go.sum @@ -1340,8 +1340,8 @@ gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZ google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.239.0 h1:2hZKUnFZEy81eugPs4e2XzIJ5SOwQg0G82bpXD65Puo= -google.golang.org/api v0.239.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= +google.golang.org/api v0.240.0 h1:PxG3AA2UIqT1ofIzWV2COM3j3JagKTKSwy7L6RHNXNU= +google.golang.org/api v0.240.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= From dfb64ae8133a2b64140a5610471f8addad1025af Mon Sep 17 00:00:00 2001 From: Ivan Ka <5395690+ivankatliarchuk@users.noreply.github.com> Date: Thu, 3 Jul 2025 10:55:26 +0100 Subject: [PATCH 11/49] chore(source): reorganise sources and wrappers (#5598) * chore(source): reorganise sources and wrappers Signed-off-by: ivan katliarchuk * chore(source): reorganise sources and wrappers Signed-off-by: ivan katliarchuk * chore(source): reorganise sources and wrappers Signed-off-by: ivan katliarchuk --------- Signed-off-by: ivan katliarchuk --- controller/execute.go | 8 +- source/endpoints.go | 11 ++ source/{ => wrappers}/dedupsource.go | 8 +- source/{ => wrappers}/dedupsource_test.go | 5 +- source/{ => wrappers}/multisource.go | 21 ++-- source/{ => wrappers}/multisource_test.go | 17 +-- source/{ => wrappers}/nat64source.go | 7 +- source/{ => wrappers}/nat64source_test.go | 5 +- source/wrappers/source_test.go | 102 ++++++++++++++++++ source/{ => wrappers}/targetfiltersource.go | 8 +- .../{ => wrappers}/targetfiltersource_test.go | 7 +- 11 files changed, 162 insertions(+), 37 deletions(-) rename source/{ => wrappers}/dedupsource.go (92%) rename source/{ => wrappers}/dedupsource_test.go (98%) rename source/{ => wrappers}/multisource.go (75%) rename source/{ => wrappers}/multisource_test.go (94%) rename source/{ => wrappers}/nat64source.go (94%) rename source/{ => wrappers}/nat64source_test.go (97%) create mode 100644 source/wrappers/source_test.go rename source/{ => wrappers}/targetfiltersource.go (90%) rename source/{ => wrappers}/targetfiltersource_test.go (96%) diff --git a/controller/execute.go b/controller/execute.go index 508a6f4e8..a69a1a7ae 100644 --- a/controller/execute.go +++ b/controller/execute.go @@ -33,6 +33,8 @@ import ( log "github.com/sirupsen/logrus" "k8s.io/klog/v2" + "sigs.k8s.io/external-dns/source/wrappers" + "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/pkg/apis/externaldns" "sigs.k8s.io/external-dns/pkg/apis/externaldns/validation" @@ -423,11 +425,11 @@ func buildSource(ctx context.Context, cfg *externaldns.Config) (source.Source, e return nil, err } // Combine multiple sources into a single, deduplicated source. - combinedSource := source.NewDedupSource(source.NewMultiSource(sources, sourceCfg.DefaultTargets, sourceCfg.ForceDefaultTargets)) + combinedSource := wrappers.NewDedupSource(wrappers.NewMultiSource(sources, sourceCfg.DefaultTargets, sourceCfg.ForceDefaultTargets)) // Filter targets targetFilter := endpoint.NewTargetNetFilterWithExclusions(cfg.TargetNetFilter, cfg.ExcludeTargetNets) - combinedSource = source.NewNAT64Source(combinedSource, cfg.NAT64Networks) - combinedSource = source.NewTargetFilterSource(combinedSource, targetFilter) + combinedSource = wrappers.NewNAT64Source(combinedSource, cfg.NAT64Networks) + combinedSource = wrappers.NewTargetFilterSource(combinedSource, targetFilter) return combinedSource, nil } diff --git a/source/endpoints.go b/source/endpoints.go index ded87c4f2..772c1b9ce 100644 --- a/source/endpoints.go +++ b/source/endpoints.go @@ -81,6 +81,17 @@ func endpointsForHostname(hostname string, targets endpoint.Targets, ttl endpoin return endpoints } +func EndpointsForHostname( + hostname string, + targets endpoint.Targets, + ttl endpoint.TTL, + providerSpecific endpoint.ProviderSpecific, + setIdentifier string, + resource string, +) []*endpoint.Endpoint { + return endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource) +} + func EndpointTargetsFromServices(svcInformer coreinformers.ServiceInformer, namespace string, selector map[string]string) (endpoint.Targets, error) { targets := endpoint.Targets{} diff --git a/source/dedupsource.go b/source/wrappers/dedupsource.go similarity index 92% rename from source/dedupsource.go rename to source/wrappers/dedupsource.go index 3ac90d5db..665799b44 100644 --- a/source/dedupsource.go +++ b/source/wrappers/dedupsource.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package source +package wrappers import ( "context" @@ -22,16 +22,18 @@ import ( log "github.com/sirupsen/logrus" + "sigs.k8s.io/external-dns/source" + "sigs.k8s.io/external-dns/endpoint" ) // dedupSource is a Source that removes duplicate endpoints from its wrapped source. type dedupSource struct { - source Source + source source.Source } // NewDedupSource creates a new dedupSource wrapping the provided Source. -func NewDedupSource(source Source) Source { +func NewDedupSource(source source.Source) source.Source { return &dedupSource{source: source} } diff --git a/source/dedupsource_test.go b/source/wrappers/dedupsource_test.go similarity index 98% rename from source/dedupsource_test.go rename to source/wrappers/dedupsource_test.go index d387f8819..07a13f40b 100644 --- a/source/dedupsource_test.go +++ b/source/wrappers/dedupsource_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package source +package wrappers import ( "context" @@ -22,10 +22,11 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/internal/testutils" + "sigs.k8s.io/external-dns/source" ) // Validates that dedupSource is a Source -var _ Source = &dedupSource{} +var _ source.Source = &dedupSource{} func TestDedup(t *testing.T) { t.Run("Endpoints", testDedupEndpoints) diff --git a/source/multisource.go b/source/wrappers/multisource.go similarity index 75% rename from source/multisource.go rename to source/wrappers/multisource.go index 80f5335b8..60c287716 100644 --- a/source/multisource.go +++ b/source/wrappers/multisource.go @@ -14,20 +14,21 @@ See the License for the specific language governing permissions and limitations under the License. */ -package source +package wrappers import ( "context" "strings" "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/source" log "github.com/sirupsen/logrus" ) // multiSource is a Source that merges the endpoints of its nested Sources. type multiSource struct { - children []Source + children []source.Source defaultTargets []string forceDefaultTargets bool } @@ -48,20 +49,20 @@ func (ms *multiSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, err continue } - for i := range endpoints { - hasSourceTargets := len(endpoints[i].Targets) > 0 + for _, ep := range endpoints { + hasSourceTargets := len(ep.Targets) > 0 if ms.forceDefaultTargets || !hasSourceTargets { - eps := endpointsForHostname(endpoints[i].DNSName, ms.defaultTargets, endpoints[i].RecordTTL, endpoints[i].ProviderSpecific, endpoints[i].SetIdentifier, "") - for _, ep := range eps { - ep.Labels = endpoints[i].Labels + eps := source.EndpointsForHostname(ep.DNSName, ms.defaultTargets, ep.RecordTTL, ep.ProviderSpecific, ep.SetIdentifier, "") + for _, e := range eps { + e.Labels = ep.Labels } result = append(result, eps...) continue } - log.Warnf("Source provided targets for %q (%s), ignoring default targets [%s] due to new behavior. Use --force-default-targets to revert to old behavior.", endpoints[i].DNSName, endpoints[i].RecordType, strings.Join(ms.defaultTargets, ", ")) - result = append(result, endpoints[i]) + log.Warnf("Source provided targets for %q (%s), ignoring default targets [%s] due to new behavior. Use --force-default-targets to revert to old behavior.", ep.DNSName, ep.RecordType, strings.Join(ms.defaultTargets, ", ")) + result = append(result, ep) } } @@ -75,6 +76,6 @@ func (ms *multiSource) AddEventHandler(ctx context.Context, handler func()) { } // NewMultiSource creates a new multiSource. -func NewMultiSource(children []Source, defaultTargets []string, forceDefaultTargets bool) Source { +func NewMultiSource(children []source.Source, defaultTargets []string, forceDefaultTargets bool) source.Source { return &multiSource{children: children, defaultTargets: defaultTargets, forceDefaultTargets: forceDefaultTargets} } diff --git a/source/multisource_test.go b/source/wrappers/multisource_test.go similarity index 94% rename from source/multisource_test.go rename to source/wrappers/multisource_test.go index 8386c75b4..06626a08c 100644 --- a/source/multisource_test.go +++ b/source/wrappers/multisource_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package source +package wrappers import ( "context" @@ -23,6 +23,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "sigs.k8s.io/external-dns/source" "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/internal/testutils" @@ -39,7 +40,7 @@ func TestMultiSource(t *testing.T) { // testMultiSourceImplementsSource tests that multiSource is a valid Source. func testMultiSourceImplementsSource(t *testing.T) { - assert.Implements(t, (*Source)(nil), new(multiSource)) + assert.Implements(t, (*source.Source)(nil), new(multiSource)) } // testMultiSourceEndpoints tests merged endpoints from children are returned. @@ -78,7 +79,7 @@ func testMultiSourceEndpoints(t *testing.T) { t.Parallel() // Prepare the nested mock sources. - sources := make([]Source, 0, len(tc.nestedEndpoints)) + sources := make([]source.Source, 0, len(tc.nestedEndpoints)) // Populate the nested mock sources. for _, endpoints := range tc.nestedEndpoints { @@ -116,7 +117,7 @@ func testMultiSourceEndpointsWithError(t *testing.T) { src.On("Endpoints").Return(nil, errSomeError) // Create our object under test and get the endpoints. - source := NewMultiSource([]Source{src}, nil, false) + source := NewMultiSource([]source.Source{src}, nil, false) // Get endpoints from our source. _, err := source.Endpoints(context.Background()) @@ -155,7 +156,7 @@ func testMultiSourceEndpointsDefaultTargets(t *testing.T) { src.On("Endpoints").Return(sourceEndpoints, nil) // Test with forceDefaultTargets=false (default behavior) - source := NewMultiSource([]Source{src}, defaultTargets, false) + source := NewMultiSource([]source.Source{src}, defaultTargets, false) endpoints, err := source.Endpoints(context.Background()) require.NoError(t, err) @@ -185,7 +186,7 @@ func testMultiSourceEndpointsDefaultTargets(t *testing.T) { src.On("Endpoints").Return(sourceEndpoints, nil) // Test with forceDefaultTargets=false (default behavior) - source := NewMultiSource([]Source{src}, defaultTargets, false) + source := NewMultiSource([]source.Source{src}, defaultTargets, false) endpoints, err := source.Endpoints(context.Background()) require.NoError(t, err) @@ -223,7 +224,7 @@ func testMultiSourceEndpointsDefaultTargets(t *testing.T) { src.On("Endpoints").Return(sourceEndpoints, nil) // Test with forceDefaultTargets=true (legacy behavior) - source := NewMultiSource([]Source{src}, defaultTargets, true) + source := NewMultiSource([]source.Source{src}, defaultTargets, true) endpoints, err := source.Endpoints(context.Background()) require.NoError(t, err) @@ -258,7 +259,7 @@ func testMultiSourceEndpointsDefaultTargets(t *testing.T) { src.On("Endpoints").Return(sourceEndpoints, nil) // Test with forceDefaultTargets=true - source := NewMultiSource([]Source{src}, defaultTargets, true) + source := NewMultiSource([]source.Source{src}, defaultTargets, true) endpoints, err := source.Endpoints(context.Background()) require.NoError(t, err) diff --git a/source/nat64source.go b/source/wrappers/nat64source.go similarity index 94% rename from source/nat64source.go rename to source/wrappers/nat64source.go index 79bcf01d8..383bdde5c 100644 --- a/source/nat64source.go +++ b/source/wrappers/nat64source.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package source +package wrappers import ( "context" @@ -22,16 +22,17 @@ import ( "net/netip" "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/source" ) // nat64Source is a Source that adds A endpoints for AAAA records including an NAT64 address. type nat64Source struct { - source Source + source source.Source nat64Prefixes []string } // NewNAT64Source creates a new nat64Source wrapping the provided Source. -func NewNAT64Source(source Source, nat64Prefixes []string) Source { +func NewNAT64Source(source source.Source, nat64Prefixes []string) source.Source { return &nat64Source{source: source, nat64Prefixes: nat64Prefixes} } diff --git a/source/nat64source_test.go b/source/wrappers/nat64source_test.go similarity index 97% rename from source/nat64source_test.go rename to source/wrappers/nat64source_test.go index d7a6a0a33..3a0d15f12 100644 --- a/source/nat64source_test.go +++ b/source/wrappers/nat64source_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package source +package wrappers import ( "context" @@ -22,10 +22,11 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/internal/testutils" + "sigs.k8s.io/external-dns/source" ) // Validates that dedupSource is a Source -var _ Source = &nat64Source{} +var _ source.Source = &nat64Source{} func TestNAT64Source(t *testing.T) { t.Run("Endpoints", testNat64Source) diff --git a/source/wrappers/source_test.go b/source/wrappers/source_test.go new file mode 100644 index 000000000..896e98af0 --- /dev/null +++ b/source/wrappers/source_test.go @@ -0,0 +1,102 @@ +/* +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 wrappers + +import ( + "reflect" + "sort" + "testing" + + "sigs.k8s.io/external-dns/endpoint" +) + +func sortEndpoints(endpoints []*endpoint.Endpoint) { + for _, ep := range endpoints { + sort.Strings([]string(ep.Targets)) + } + sort.Slice(endpoints, func(i, k int) bool { + // Sort by DNSName, RecordType, and Targets + ei, ek := endpoints[i], endpoints[k] + if ei.DNSName != ek.DNSName { + return ei.DNSName < ek.DNSName + } + if ei.RecordType != ek.RecordType { + return ei.RecordType < ek.RecordType + } + // Targets are sorted ahead of time. + for j, ti := range ei.Targets { + if j >= len(ek.Targets) { + return true + } + if tk := ek.Targets[j]; ti != tk { + return ti < tk + } + } + return false + }) +} + +func validateEndpoints(t *testing.T, endpoints, expected []*endpoint.Endpoint) { + t.Helper() + + if len(endpoints) != len(expected) { + t.Fatalf("expected %d endpoints, got %d", len(expected), len(endpoints)) + } + + // Make sure endpoints are sorted - validateEndpoint() depends on it. + sortEndpoints(endpoints) + sortEndpoints(expected) + + for i := range endpoints { + validateEndpoint(t, endpoints[i], expected[i]) + } +} + +func validateEndpoint(t *testing.T, endpoint, expected *endpoint.Endpoint) { + t.Helper() + + if endpoint.DNSName != expected.DNSName { + t.Errorf("DNSName expected %q, got %q", expected.DNSName, endpoint.DNSName) + } + + if !endpoint.Targets.Same(expected.Targets) { + t.Errorf("Targets expected %q, got %q", expected.Targets, endpoint.Targets) + } + + if endpoint.RecordTTL != expected.RecordTTL { + t.Errorf("RecordTTL expected %v, got %v", expected.RecordTTL, endpoint.RecordTTL) + } + + // if a non-empty record type is expected, check that it matches. + if endpoint.RecordType != expected.RecordType { + t.Errorf("RecordType expected %q, got %q", expected.RecordType, endpoint.RecordType) + } + + // if non-empty labels are expected, check that they match. + if expected.Labels != nil && !reflect.DeepEqual(endpoint.Labels, expected.Labels) { + t.Errorf("Labels expected %s, got %s", expected.Labels, endpoint.Labels) + } + + if (len(expected.ProviderSpecific) != 0 || len(endpoint.ProviderSpecific) != 0) && + !reflect.DeepEqual(endpoint.ProviderSpecific, expected.ProviderSpecific) { + t.Errorf("ProviderSpecific expected %s, got %s", expected.ProviderSpecific, endpoint.ProviderSpecific) + } + + if endpoint.SetIdentifier != expected.SetIdentifier { + t.Errorf("SetIdentifier expected %q, got %q", expected.SetIdentifier, endpoint.SetIdentifier) + } +} diff --git a/source/targetfiltersource.go b/source/wrappers/targetfiltersource.go similarity index 90% rename from source/targetfiltersource.go rename to source/wrappers/targetfiltersource.go index e20273606..fe6d1d283 100644 --- a/source/targetfiltersource.go +++ b/source/wrappers/targetfiltersource.go @@ -14,24 +14,26 @@ See the License for the specific language governing permissions and limitations under the License. */ -package source +package wrappers import ( "context" log "github.com/sirupsen/logrus" + source2 "sigs.k8s.io/external-dns/source" + "sigs.k8s.io/external-dns/endpoint" ) // targetFilterSource is a Source that removes endpoints matching the target filter from its wrapped source. type targetFilterSource struct { - source Source + source source2.Source targetFilter endpoint.TargetFilterInterface } // NewTargetFilterSource creates a new targetFilterSource wrapping the provided Source. -func NewTargetFilterSource(source Source, targetFilter endpoint.TargetFilterInterface) Source { +func NewTargetFilterSource(source source2.Source, targetFilter endpoint.TargetFilterInterface) source2.Source { return &targetFilterSource{source: source, targetFilter: targetFilter} } diff --git a/source/targetfiltersource_test.go b/source/wrappers/targetfiltersource_test.go similarity index 96% rename from source/targetfiltersource_test.go rename to source/wrappers/targetfiltersource_test.go index b1f6fc718..733c1e1a3 100644 --- a/source/targetfiltersource_test.go +++ b/source/wrappers/targetfiltersource_test.go @@ -14,13 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -package source +package wrappers import ( "testing" "github.com/stretchr/testify/require" "golang.org/x/net/context" + "sigs.k8s.io/external-dns/source" "sigs.k8s.io/external-dns/endpoint" ) @@ -55,7 +56,7 @@ func (e *echoSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error } // NewEchoSource creates a new echoSource. -func NewEchoSource(endpoints []*endpoint.Endpoint) Source { +func NewEchoSource(endpoints []*endpoint.Endpoint) source.Source { return &echoSource{endpoints: endpoints} } @@ -90,7 +91,7 @@ func TestTargetFilterSource(t *testing.T) { // TestTargetFilterSourceImplementsSource tests that targetFilterSource is a valid Source. func TestTargetFilterSourceImplementsSource(t *testing.T) { - var _ Source = &targetFilterSource{} + var _ source.Source = &targetFilterSource{} } func TestTargetFilterSourceEndpoints(t *testing.T) { From d79dd835af2d1c74d10c57dcd26e4d4a81ad2324 Mon Sep 17 00:00:00 2001 From: Prasad Katti Date: Thu, 3 Jul 2025 08:19:26 -0700 Subject: [PATCH 12/49] feat(aws): add support for geoproximity routing (#5347) * feat(aws): add support for geoproximity routing * remove the invalid test * make some changes based on review comments * fix linting errors * make changes based on review feedback * add more tests to get better coverage * update docs * make the linter happy * address review feedback This commit addresses the review feedback by making the following changes: - use a more object-oriented approach for geoProximity handling - change log levels to warnings instead of errors - add more test cases for geoProximity * fix linting error * use shorter annotation names --- docs/tutorials/aws.md | 5 + provider/aws/aws.go | 172 ++++++++++++++- provider/aws/aws_test.go | 449 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 614 insertions(+), 12 deletions(-) diff --git a/docs/tutorials/aws.md b/docs/tutorials/aws.md index a7a9a8034..141ed7d1c 100644 --- a/docs/tutorials/aws.md +++ b/docs/tutorials/aws.md @@ -894,6 +894,11 @@ For any given DNS name, only **one** of the following routing policies can be us - `external-dns.alpha.kubernetes.io/aws-geolocation-continent-code` - `external-dns.alpha.kubernetes.io/aws-geolocation-country-code` - `external-dns.alpha.kubernetes.io/aws-geolocation-subdivision-code` +- Geoproximity routing: + - `external-dns.alpha.kubernetes.io/aws-geoproximity-region` + - `external-dns.alpha.kubernetes.io/aws-geoproximity-local-zone-group` + - `external-dns.alpha.kubernetes.io/aws-geoproximity-coordinates` + - `external-dns.alpha.kubernetes.io/aws-geoproximity-bias` - Multi-value answer:`external-dns.alpha.kubernetes.io/aws-multi-value-answer` ### Associating DNS records with healthchecks diff --git a/provider/aws/aws.go b/provider/aws/aws.go index 458b33089..b27ba209d 100644 --- a/provider/aws/aws.go +++ b/provider/aws/aws.go @@ -53,19 +53,27 @@ const ( // providerSpecificEvaluateTargetHealth specifies whether an AWS ALIAS record // has the EvaluateTargetHealth field set to true. Present iff the endpoint // has a `providerSpecificAlias` value of `true`. - providerSpecificEvaluateTargetHealth = "aws/evaluate-target-health" - providerSpecificWeight = "aws/weight" - providerSpecificRegion = "aws/region" - providerSpecificFailover = "aws/failover" - providerSpecificGeolocationContinentCode = "aws/geolocation-continent-code" - providerSpecificGeolocationCountryCode = "aws/geolocation-country-code" - providerSpecificGeolocationSubdivisionCode = "aws/geolocation-subdivision-code" - providerSpecificMultiValueAnswer = "aws/multi-value-answer" - providerSpecificHealthCheckID = "aws/health-check-id" - sameZoneAlias = "same-zone" + providerSpecificEvaluateTargetHealth = "aws/evaluate-target-health" + providerSpecificWeight = "aws/weight" + providerSpecificRegion = "aws/region" + providerSpecificFailover = "aws/failover" + providerSpecificGeolocationContinentCode = "aws/geolocation-continent-code" + providerSpecificGeolocationCountryCode = "aws/geolocation-country-code" + providerSpecificGeolocationSubdivisionCode = "aws/geolocation-subdivision-code" + providerSpecificGeoProximityLocationAWSRegion = "aws/geoproximity-region" + providerSpecificGeoProximityLocationBias = "aws/geoproximity-bias" + providerSpecificGeoProximityLocationCoordinates = "aws/geoproximity-coordinates" + providerSpecificGeoProximityLocationLocalZoneGroup = "aws/geoproximity-local-zone-group" + providerSpecificMultiValueAnswer = "aws/multi-value-answer" + providerSpecificHealthCheckID = "aws/health-check-id" + sameZoneAlias = "same-zone" // Currently supported up to 10 health checks or hosted zones. // https://docs.aws.amazon.com/Route53/latest/APIReference/API_ListTagsForResources.html#API_ListTagsForResources_RequestSyntax - batchSize = 10 + batchSize = 10 + minLatitude = -90.0 + maxLatitude = 90.0 + minLongitude = -180.0 + maxLongitude = 180.0 ) // see elb: https://docs.aws.amazon.com/general/latest/gr/elb.html @@ -231,6 +239,12 @@ type profiledZone struct { zone *route53types.HostedZone } +type geoProximity struct { + location *route53types.GeoProximityLocation + endpoint *endpoint.Endpoint + isSet bool +} + func (cs Route53Changes) Route53Changes() []route53types.Change { var ret []route53types.Change for _, c := range cs { @@ -542,6 +556,8 @@ func (p *AWSProvider) records(ctx context.Context, zones map[string]*profiledZon ep.WithProviderSpecific(providerSpecificGeolocationSubdivisionCode, *r.GeoLocation.SubdivisionCode) } } + case r.GeoProximityLocation != nil: + handleGeoProximityLocationRecord(&r, ep) default: // one of the above needs to be set, otherwise SetIdentifier doesn't make sense } @@ -560,6 +576,25 @@ func (p *AWSProvider) records(ctx context.Context, zones map[string]*profiledZon return endpoints, nil } +func handleGeoProximityLocationRecord(r *route53types.ResourceRecordSet, ep *endpoint.Endpoint) { + if region := aws.ToString(r.GeoProximityLocation.AWSRegion); region != "" { + ep.WithProviderSpecific(providerSpecificGeoProximityLocationAWSRegion, region) + } + + if bias := r.GeoProximityLocation.Bias; bias != nil { + ep.WithProviderSpecific(providerSpecificGeoProximityLocationBias, fmt.Sprintf("%d", aws.ToInt32(bias))) + } + + if coords := r.GeoProximityLocation.Coordinates; coords != nil { + coordinates := fmt.Sprintf("%s,%s", aws.ToString(coords.Latitude), aws.ToString(coords.Longitude)) + ep.WithProviderSpecific(providerSpecificGeoProximityLocationCoordinates, coordinates) + } + + if localZoneGroup := aws.ToString(r.GeoProximityLocation.LocalZoneGroup); localZoneGroup != "" { + ep.WithProviderSpecific(providerSpecificGeoProximityLocationLocalZoneGroup, localZoneGroup) + } +} + // Identify if old and new endpoints require DELETE/CREATE instead of UPDATE. func (p *AWSProvider) requiresDeleteCreate(old *endpoint.Endpoint, newE *endpoint.Endpoint) bool { // a change of a record type @@ -832,12 +867,32 @@ func (p *AWSProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) ([]*endpoi } else { ep.DeleteProviderSpecificProperty(providerSpecificEvaluateTargetHealth) } + + adjustGeoProximityLocationEndpoint(ep) } endpoints = append(endpoints, aliasCnameAaaaEndpoints...) return endpoints, nil } +// if the endpoint is using geoproximity, set the bias to 0 if not set +// this is needed to avoid unnecessary Upserts if the desired endpoint doesn't specify a bias +func adjustGeoProximityLocationEndpoint(ep *endpoint.Endpoint) { + if ep.SetIdentifier == "" { + return + } + _, ok1 := ep.GetProviderSpecificProperty(providerSpecificGeoProximityLocationAWSRegion) + _, ok2 := ep.GetProviderSpecificProperty(providerSpecificGeoProximityLocationLocalZoneGroup) + _, ok3 := ep.GetProviderSpecificProperty(providerSpecificGeoProximityLocationCoordinates) + + if ok1 || ok2 || ok3 { + // check if ep has bias property and if not, set it to 0 + if _, ok := ep.GetProviderSpecificProperty(providerSpecificGeoProximityLocationBias); !ok { + ep.SetProviderSpecificProperty(providerSpecificGeoProximityLocationBias, "0") + } + } +} + // newChange returns a route53 Change // returned Change is based on the given record by the given action, e.g. // action=ChangeActionCreate returns a change for creation of the record and @@ -926,6 +981,8 @@ func (p *AWSProvider) newChange(action route53types.ChangeAction, ep *endpoint.E if useGeolocation { change.ResourceRecordSet.GeoLocation = geolocation } + + withChangeForGeoProximityEndpoint(change, ep) } if prop, ok := ep.GetProviderSpecificProperty(providerSpecificHealthCheckID); ok { @@ -939,6 +996,99 @@ func (p *AWSProvider) newChange(action route53types.ChangeAction, ep *endpoint.E return change } +func newGeoProximity(ep *endpoint.Endpoint) *geoProximity { + return &geoProximity{ + location: &route53types.GeoProximityLocation{}, + endpoint: ep, + isSet: false, + } +} + +func (gp *geoProximity) withAWSRegion() *geoProximity { + if prop, ok := gp.endpoint.GetProviderSpecificProperty(providerSpecificGeoProximityLocationAWSRegion); ok { + gp.location.AWSRegion = aws.String(prop) + gp.isSet = true + } + return gp +} + +// add a method to set the local zone group for the geoproximity location +func (gp *geoProximity) withLocalZoneGroup() *geoProximity { + if prop, ok := gp.endpoint.GetProviderSpecificProperty(providerSpecificGeoProximityLocationLocalZoneGroup); ok { + gp.location.LocalZoneGroup = aws.String(prop) + gp.isSet = true + } + return gp +} + +// add a method to set the bias for the geoproximity location +func (gp *geoProximity) withBias() *geoProximity { + if prop, ok := gp.endpoint.GetProviderSpecificProperty(providerSpecificGeoProximityLocationBias); ok { + bias, err := strconv.ParseInt(prop, 10, 32) + if err != nil { + log.Warnf("Failed parsing value of %s: %s: %v; using bias of 0", providerSpecificGeoProximityLocationBias, prop, err) + bias = 0 + } + gp.location.Bias = aws.Int32(int32(bias)) + gp.isSet = true + } + return gp +} + +// validateCoordinates checks if the given latitude and longitude are valid. +func validateCoordinates(lat, long string) error { + latitude, err := strconv.ParseFloat(lat, 64) + if err != nil || latitude < minLatitude || latitude > maxLatitude { + return fmt.Errorf("invalid latitude: must be a number between %f and %f", minLatitude, maxLatitude) + } + + longitude, err := strconv.ParseFloat(long, 64) + if err != nil || longitude < minLongitude || longitude > maxLongitude { + return fmt.Errorf("invalid longitude: must be a number between %f and %f", minLongitude, maxLongitude) + } + + return nil +} + +func (gp *geoProximity) withCoordinates() *geoProximity { + if prop, ok := gp.endpoint.GetProviderSpecificProperty(providerSpecificGeoProximityLocationCoordinates); ok { + coordinates := strings.Split(prop, ",") + if len(coordinates) == 2 { + latitude := coordinates[0] + longitude := coordinates[1] + if err := validateCoordinates(latitude, longitude); err != nil { + log.Warnf("Invalid coordinates %s for name=%s setIdentifier=%s; %v", prop, gp.endpoint.DNSName, gp.endpoint.SetIdentifier, err) + } else { + gp.location.Coordinates = &route53types.Coordinates{ + Latitude: aws.String(latitude), + Longitude: aws.String(longitude), + } + gp.isSet = true + } + } else { + log.Warnf("Invalid coordinates format for %s: %s; expected format 'latitude,longitude'", providerSpecificGeoProximityLocationCoordinates, prop) + } + } + return gp +} + +func (gp *geoProximity) build() *route53types.GeoProximityLocation { + if gp.isSet { + return gp.location + } + return nil +} + +func withChangeForGeoProximityEndpoint(change *Route53Change, ep *endpoint.Endpoint) { + geoProx := newGeoProximity(ep). + withAWSRegion(). + withCoordinates(). + withLocalZoneGroup(). + withBias() + + change.ResourceRecordSet.GeoProximityLocation = geoProx.build() +} + // searches for `changes` that are contained in `queue` and returns the `changes` separated by whether they were found in the queue (`foundChanges`) or not (`notFoundChanges`) func findChangesInQueue(changes Route53Changes, queue Route53Changes) (Route53Changes, Route53Changes) { if queue == nil { diff --git a/provider/aws/aws_test.go b/provider/aws/aws_test.go index 500d37161..474c97250 100644 --- a/provider/aws/aws_test.go +++ b/provider/aws/aws_test.go @@ -583,6 +583,42 @@ func TestAWSRecords(t *testing.T) { SubdivisionCode: aws.String("NY"), }, }, + { + Name: aws.String("geoproximitylocation-region.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: route53types.RRTypeA, + TTL: aws.Int64(defaultTTL), + ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}}, + SetIdentifier: aws.String("test-set-1"), + GeoProximityLocation: &route53types.GeoProximityLocation{ + AWSRegion: aws.String("us-west-2"), + Bias: aws.Int32(10), + }, + }, + { + Name: aws.String("geoproximitylocation-localzone.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: route53types.RRTypeA, + TTL: aws.Int64(defaultTTL), + ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}}, + SetIdentifier: aws.String("test-set-1"), + GeoProximityLocation: &route53types.GeoProximityLocation{ + LocalZoneGroup: aws.String("usw2-pdx1-az1"), + Bias: aws.Int32(10), + }, + }, + { + Name: aws.String("geoproximitylocation-coordinates.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: route53types.RRTypeA, + TTL: aws.Int64(defaultTTL), + ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}}, + SetIdentifier: aws.String("test-set-1"), + GeoProximityLocation: &route53types.GeoProximityLocation{ + Coordinates: &route53types.Coordinates{ + Latitude: aws.String("90"), + Longitude: aws.String("90"), + }, + Bias: aws.Int32(0), + }, + }, { Name: aws.String("healthcheck-test.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeCname, @@ -636,6 +672,9 @@ func TestAWSRecords(t *testing.T) { endpoint.NewEndpointWithTTL("geolocation-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeolocationContinentCode, "EU"), endpoint.NewEndpointWithTTL("geolocation-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "4.3.2.1").WithSetIdentifier("test-set-2").WithProviderSpecific(providerSpecificGeolocationCountryCode, "DE"), endpoint.NewEndpointWithTTL("geolocation-subdivision-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeolocationSubdivisionCode, "NY"), + endpoint.NewEndpointWithTTL("geoproximitylocation-region.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeoProximityLocationAWSRegion, "us-west-2").WithProviderSpecific(providerSpecificGeoProximityLocationBias, "10"), + endpoint.NewEndpointWithTTL("geoproximitylocation-localzone.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeoProximityLocationLocalZoneGroup, "usw2-pdx1-az1").WithProviderSpecific(providerSpecificGeoProximityLocationBias, "10"), + endpoint.NewEndpointWithTTL("geoproximitylocation-coordinates.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeoProximityLocationCoordinates, "90,90").WithProviderSpecific(providerSpecificGeoProximityLocationBias, "0"), endpoint.NewEndpointWithTTL("healthcheck-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(defaultTTL), "foo.example.com").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificWeight, "10").WithProviderSpecific(providerSpecificHealthCheckID, "foo-bar-healthcheck-id").WithProviderSpecific(providerSpecificAlias, "false"), endpoint.NewEndpointWithTTL("healthcheck-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(defaultTTL), "4.3.2.1").WithSetIdentifier("test-set-2").WithProviderSpecific(providerSpecificWeight, "20").WithProviderSpecific(providerSpecificHealthCheckID, "abc-def-healthcheck-id"), endpoint.NewEndpointWithTTL("mail.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, endpoint.TTL(defaultTTL), "10 mailhost1.example.com", "20 mailhost2.example.com"), @@ -670,6 +709,7 @@ func TestAWSAdjustEndpoints(t *testing.T) { endpoint.NewEndpoint("cname-test-elb-no-alias.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "false"), endpoint.NewEndpoint("cname-test-elb-no-eth.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false"), // eth = evaluate target health endpoint.NewEndpoint("cname-test-elb-alias.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true"), + endpoint.NewEndpoint("a-test-geoproximity-no-bias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeoProximityLocationAWSRegion, "us-west-2"), } records, err := provider.AdjustEndpoints(records) @@ -687,6 +727,7 @@ func TestAWSAdjustEndpoints(t *testing.T) { endpoint.NewEndpoint("cname-test-elb-no-eth.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false"), // eth = evaluate target health endpoint.NewEndpoint("cname-test-elb-alias.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true"), endpoint.NewEndpoint("cname-test-elb-alias.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true"), + endpoint.NewEndpoint("a-test-geoproximity-no-bias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeoProximityLocationAWSRegion, "us-west-2").WithProviderSpecific(providerSpecificGeoProximityLocationBias, "0"), }) } @@ -845,6 +886,27 @@ func TestAWSApplyChanges(t *testing.T) { TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("2606:4700:4700::1111")}, {Value: aws.String("2606:4700:4700::1001")}}, }, + { + Name: aws.String("delete-test-geoproximity.zone-2.ext-dns-test-2.teapot.zalan.do."), + Type: route53types.RRTypeA, + TTL: aws.Int64(defaultTTL), + ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}}, + SetIdentifier: aws.String("geoproximity-delete"), + GeoProximityLocation: &route53types.GeoProximityLocation{ + AWSRegion: aws.String("us-west-2"), + Bias: aws.Int32(10), + }, + }, + { + Name: aws.String("update-test-geoproximity.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: route53types.RRTypeA, + TTL: aws.Int64(defaultTTL), + ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}}, + SetIdentifier: aws.String("geoproximity-update"), + GeoProximityLocation: &route53types.GeoProximityLocation{ + LocalZoneGroup: aws.String("usw2-lax1-az2"), + }, + }, { Name: aws.String("weighted-to-simple.zone-1.ext-dns-test-2.teapot.zalan.do."), Type: route53types.RRTypeA, @@ -915,6 +977,13 @@ func TestAWSApplyChanges(t *testing.T) { endpoint.NewEndpoint("create-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8", "8.8.4.4"), endpoint.NewEndpoint("create-test-multiple-aaaa.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "2606:4700:4700::1111", "2606:4700:4700::1001"), endpoint.NewEndpoint("create-test-mx.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "10 mailhost1.foo.elb.amazonaws.com"), + endpoint.NewEndpoint("create-test-geoproximity-region.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"). + WithSetIdentifier("geoproximity-region"). + WithProviderSpecific(providerSpecificGeoProximityLocationAWSRegion, "us-west-2"). + WithProviderSpecific(providerSpecificGeoProximityLocationBias, "10"), + endpoint.NewEndpoint("create-test-geoproximity-coordinates.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"). + WithSetIdentifier("geoproximity-coordinates"). + WithProviderSpecific(providerSpecificGeoProximityLocationCoordinates, "60,60"), } currentRecords := []*endpoint.Endpoint{ @@ -930,6 +999,9 @@ func TestAWSApplyChanges(t *testing.T) { endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "bar.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true"), endpoint.NewEndpoint("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8", "8.8.4.4"), endpoint.NewEndpoint("update-test-multiple-aaaa.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "2606:4700:4700::1111", "2606:4700:4700::1001"), + endpoint.NewEndpoint("update-test-geoproximity.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"). + WithSetIdentifier("geoproximity-update"). + WithProviderSpecific(providerSpecificGeoProximityLocationLocalZoneGroup, "usw2-lax1-az2"), endpoint.NewEndpoint("weighted-to-simple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("weighted-to-simple").WithProviderSpecific(providerSpecificWeight, "10"), endpoint.NewEndpoint("simple-to-weighted.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"), endpoint.NewEndpoint("policy-change.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("policy-change").WithProviderSpecific(providerSpecificWeight, "10"), @@ -951,6 +1023,9 @@ func TestAWSApplyChanges(t *testing.T) { endpoint.NewEndpoint("update-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "baz.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true"), endpoint.NewEndpoint("update-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4", "4.3.2.1"), endpoint.NewEndpoint("update-test-multiple-aaaa.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "2606:4700:4700::1001", "2606:4700:4700::1111"), + endpoint.NewEndpoint("update-test-geoproximity.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"). + WithSetIdentifier("geoproximity-update"). + WithProviderSpecific(providerSpecificGeoProximityLocationLocalZoneGroup, "usw2-phx2-az1"), endpoint.NewEndpoint("weighted-to-simple.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4"), endpoint.NewEndpoint("simple-to-weighted.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("simple-to-weighted").WithProviderSpecific(providerSpecificWeight, "10"), endpoint.NewEndpoint("policy-change.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("policy-change").WithProviderSpecific(providerSpecificRegion, "us-east-1"), @@ -969,6 +1044,7 @@ func TestAWSApplyChanges(t *testing.T) { endpoint.NewEndpoint("delete-test-cname-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "qux.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true"), endpoint.NewEndpoint("delete-test-multiple.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4", "4.3.2.1"), endpoint.NewEndpoint("delete-test-multiple-aaaa.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeAAAA, "2606:4700:4700::1111", "2606:4700:4700::1001"), + endpoint.NewEndpoint("delete-test-geoproximity.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "1.2.3.4").WithSetIdentifier("geoproximity-delete").WithProviderSpecific(providerSpecificGeoProximityLocationAWSRegion, "us-west-2").WithProviderSpecific(providerSpecificGeoProximityLocationBias, "10"), endpoint.NewEndpoint("delete-test-mx.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, "30 mailhost1.foo.elb.amazonaws.com"), } @@ -1118,6 +1194,40 @@ func TestAWSApplyChanges(t *testing.T) { TTL: aws.Int64(defaultTTL), ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("10 mailhost1.foo.elb.amazonaws.com")}}, }, + { + Name: aws.String("create-test-geoproximity-region.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: route53types.RRTypeA, + TTL: aws.Int64(defaultTTL), + ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("8.8.8.8")}}, + SetIdentifier: aws.String("geoproximity-region"), + GeoProximityLocation: &route53types.GeoProximityLocation{ + AWSRegion: aws.String("us-west-2"), + Bias: aws.Int32(10), + }, + }, + { + Name: aws.String("update-test-geoproximity.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: route53types.RRTypeA, + TTL: aws.Int64(defaultTTL), + ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("1.2.3.4")}}, + SetIdentifier: aws.String("geoproximity-update"), + GeoProximityLocation: &route53types.GeoProximityLocation{ + LocalZoneGroup: aws.String("usw2-phx2-az1"), + }, + }, + { + Name: aws.String("create-test-geoproximity-coordinates.zone-1.ext-dns-test-2.teapot.zalan.do."), + Type: route53types.RRTypeA, + TTL: aws.Int64(defaultTTL), + ResourceRecords: []route53types.ResourceRecord{{Value: aws.String("8.8.8.8")}}, + SetIdentifier: aws.String("geoproximity-coordinates"), + GeoProximityLocation: &route53types.GeoProximityLocation{ + Coordinates: &route53types.Coordinates{ + Latitude: aws.String("60"), + Longitude: aws.String("60"), + }, + }, + }, }) validateRecords(t, listAWSRecords(t, provider.clients[defaultAWSProfile], "/hostedzone/zone-2.ext-dns-test-2.teapot.zalan.do."), []route53types.ResourceRecordSet{ { @@ -1902,7 +2012,7 @@ func validateEndpoints(t *testing.T, provider *AWSProvider, endpoints []*endpoin normalized, err := provider.AdjustEndpoints(endpoints) assert.NoError(t, err) - assert.True(t, testutils.SameEndpoints(normalized, expected), "actual and normalized endpoints don't match. %+v:%+v", endpoints, normalized) + assert.True(t, testutils.SameEndpoints(normalized, expected), "normalized and expected endpoints don't match. %+v:%+v", normalized, expected) } func validateAWSZones(t *testing.T, zones map[string]*route53types.HostedZone, expected map[string]*route53types.HostedZone) { @@ -2370,3 +2480,340 @@ func TestConvertOctalToAscii(t *testing.T) { }) } } + +func TestGeoProximityWithAWSRegion(t *testing.T) { + tests := []struct { + name string + region string + hasRegion bool + expectedSet bool + expectedRegion string + }{ + { + name: "valid AWS region", + region: "us-west-2", + hasRegion: true, + expectedSet: true, + expectedRegion: "us-west-2", + }, + { + name: "another valid AWS region", + region: "eu-central-1", + hasRegion: true, + expectedSet: true, + expectedRegion: "eu-central-1", + }, + { + name: "empty region string", + region: "", + hasRegion: true, + expectedSet: true, + expectedRegion: "", + }, + { + name: "no region property set", + region: "", + hasRegion: false, + expectedSet: false, + expectedRegion: "", + }, + { + name: "region with special characters", + region: "us-gov-west-1", + hasRegion: true, + expectedSet: true, + expectedRegion: "us-gov-west-1", + }, + { + name: "region with numbers", + region: "ap-southeast-3", + hasRegion: true, + expectedSet: true, + expectedRegion: "ap-southeast-3", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ep := &endpoint.Endpoint{ + DNSName: "test.example.com", + SetIdentifier: "test-set", + } + + if tt.hasRegion { + ep.SetProviderSpecificProperty(providerSpecificGeoProximityLocationAWSRegion, tt.region) + } + + gp := newGeoProximity(ep) + result := gp.withAWSRegion() + + assert.Equal(t, tt.expectedSet, result.isSet) + + if tt.expectedSet { + assert.NotNil(t, result.location.AWSRegion) + assert.Equal(t, tt.expectedRegion, *result.location.AWSRegion) + } else { + assert.Nil(t, result.location.AWSRegion) + } + + // Verify the method returns the same instance for chaining + assert.Equal(t, gp, result) + }) + } +} + +func TestGeoProximityWithLocalZoneGroup(t *testing.T) { + tests := []struct { + name string + localZoneGroup string + hasLocalZoneGroup bool + expectedSet bool + expectedLocalZoneGroup string + }{ + { + name: "valid local zone group", + localZoneGroup: "usw2-lax1-az1", + hasLocalZoneGroup: true, + expectedSet: true, + expectedLocalZoneGroup: "usw2-lax1-az1", + }, + { + name: "empty local zone group", + localZoneGroup: "", + hasLocalZoneGroup: true, + expectedSet: true, + expectedLocalZoneGroup: "", + }, + { + name: "no local zone group property", + localZoneGroup: "", + hasLocalZoneGroup: false, + expectedSet: false, + expectedLocalZoneGroup: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ep := &endpoint.Endpoint{ + DNSName: "test.example.com", + SetIdentifier: "test-set", + } + + if tt.hasLocalZoneGroup { + ep.SetProviderSpecificProperty(providerSpecificGeoProximityLocationLocalZoneGroup, tt.localZoneGroup) + } + + gp := newGeoProximity(ep) + result := gp.withLocalZoneGroup() + + assert.Equal(t, tt.expectedSet, result.isSet) + + if tt.expectedSet { + assert.NotNil(t, result.location.LocalZoneGroup) + assert.Equal(t, tt.expectedLocalZoneGroup, *result.location.LocalZoneGroup) + } else { + assert.Nil(t, result.location.LocalZoneGroup) + } + + // Verify method returns same instance for chaining + assert.Equal(t, gp, result) + }) + } +} + +func TestGeoProximityWithCoordinates(t *testing.T) { + tests := []struct { + name string + coordinates string + expectedSet bool + expectedLat string + expectedLong string + shouldHaveCoords bool + }{ + { + name: "valid coordinates", + coordinates: "45.0,90.0", + expectedSet: true, + expectedLat: "45.0", + expectedLong: "90.0", + shouldHaveCoords: true, + }, + { + name: "edge case min coordinates", + coordinates: "-90.0,-180.0", + expectedSet: true, + expectedLat: "-90.0", + expectedLong: "-180.0", + shouldHaveCoords: true, + }, + { + name: "edge case max coordinates", + coordinates: "90.0,180.0", + expectedSet: true, + expectedLat: "90.0", + expectedLong: "180.0", + shouldHaveCoords: true, + }, + { + name: "invalid latitude too high", + coordinates: "91.0,90.0", + expectedSet: false, + shouldHaveCoords: false, + }, + { + name: "invalid longitude too low", + coordinates: "45.0,-181.0", + expectedSet: false, + shouldHaveCoords: false, + }, + { + name: "invalid format - single value", + coordinates: "45.0", + expectedSet: false, + shouldHaveCoords: false, + }, + { + name: "invalid format - three values", + coordinates: "45.0,90.0,10.0", + expectedSet: false, + shouldHaveCoords: false, + }, + { + name: "invalid format - non-numeric", + coordinates: "abc,def", + expectedSet: false, + shouldHaveCoords: false, + }, + { + name: "no coordinates property", + coordinates: "", + expectedSet: false, + shouldHaveCoords: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ep := &endpoint.Endpoint{} + if tt.coordinates != "" { + ep.SetProviderSpecificProperty(providerSpecificGeoProximityLocationCoordinates, tt.coordinates) + } + + gp := newGeoProximity(ep) + result := gp.withCoordinates() + + assert.Equal(t, tt.expectedSet, result.isSet) + + if tt.shouldHaveCoords { + assert.NotNil(t, result.location.Coordinates) + assert.Equal(t, tt.expectedLat, *result.location.Coordinates.Latitude) + assert.Equal(t, tt.expectedLong, *result.location.Coordinates.Longitude) + } else { + assert.Nil(t, result.location.Coordinates) + } + }) + } +} + +func TestGeoProximityWithBias(t *testing.T) { + tests := []struct { + name string + bias string + hasBias bool + expectedSet bool + expectedBias int32 + }{ + { + name: "valid positive bias", + bias: "10", + hasBias: true, + expectedSet: true, + expectedBias: 10, + }, + { + name: "valid negative bias", + bias: "-5", + hasBias: true, + expectedSet: true, + expectedBias: -5, + }, + { + name: "zero bias", + bias: "0", + hasBias: true, + expectedSet: true, + expectedBias: 0, + }, + { + name: "large positive bias", + bias: "99", + hasBias: true, + expectedSet: true, + expectedBias: 99, + }, + { + name: "large negative bias", + bias: "-99", + hasBias: true, + expectedSet: true, + expectedBias: -99, + }, + { + name: "invalid bias - non-numeric", + bias: "abc", + hasBias: true, + expectedSet: true, + expectedBias: 0, // defaults to 0 on error + }, + { + name: "invalid bias - float", + bias: "10.5", + hasBias: true, + expectedSet: true, + expectedBias: 0, // defaults to 0 on error + }, + { + name: "empty bias string", + bias: "", + hasBias: true, + expectedSet: true, + expectedBias: 0, // defaults to 0 on error + }, + { + name: "no bias property", + bias: "", + hasBias: false, + expectedSet: false, + expectedBias: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ep := &endpoint.Endpoint{ + DNSName: "test.example.com", + SetIdentifier: "test-set", + } + + if tt.hasBias { + ep.SetProviderSpecificProperty(providerSpecificGeoProximityLocationBias, tt.bias) + } + + gp := newGeoProximity(ep) + result := gp.withBias() + + assert.Equal(t, tt.expectedSet, result.isSet) + + if tt.expectedSet { + assert.NotNil(t, result.location.Bias) + assert.Equal(t, tt.expectedBias, *result.location.Bias) + } else { + assert.Nil(t, result.location.Bias) + } + + // Verify method returns same instance for chaining + assert.Equal(t, gp, result) + }) + } +} From fc4a2cb6acf641a39da9f92caf2c06db15268787 Mon Sep 17 00:00:00 2001 From: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com> Date: Thu, 3 Jul 2025 17:45:26 +0200 Subject: [PATCH 13/49] chore(ci): update labels automation (#5580) --- apis/OWNERS | 4 ++++ controller/OWNERS | 4 ++++ internal/OWNERS | 4 ++++ kustomize/OWNERS | 4 ++++ pkg/apis/OWNERS | 4 ++++ pkg/metrics/OWNERS | 4 ++++ pkg/rfc2317/OWNERS | 4 ++++ pkg/tlsutils/OWNERS | 4 ++++ plan/OWNERS | 4 ++++ registry/OWNERS | 4 ++++ scripts/OWNERS | 4 ++++ 11 files changed, 44 insertions(+) create mode 100644 apis/OWNERS create mode 100644 controller/OWNERS create mode 100644 internal/OWNERS create mode 100644 kustomize/OWNERS create mode 100644 pkg/apis/OWNERS create mode 100644 pkg/metrics/OWNERS create mode 100644 pkg/rfc2317/OWNERS create mode 100644 pkg/tlsutils/OWNERS create mode 100644 plan/OWNERS create mode 100644 registry/OWNERS create mode 100644 scripts/OWNERS diff --git a/apis/OWNERS b/apis/OWNERS new file mode 100644 index 000000000..16088024f --- /dev/null +++ b/apis/OWNERS @@ -0,0 +1,4 @@ +# See the OWNERS docs at https://go.k8s.io/owners + +labels: +- apis diff --git a/controller/OWNERS b/controller/OWNERS new file mode 100644 index 000000000..5e51c9fef --- /dev/null +++ b/controller/OWNERS @@ -0,0 +1,4 @@ +# See the OWNERS docs at https://go.k8s.io/owners + +labels: +- controller diff --git a/internal/OWNERS b/internal/OWNERS new file mode 100644 index 000000000..b7cb86a46 --- /dev/null +++ b/internal/OWNERS @@ -0,0 +1,4 @@ +# See the OWNERS docs at https://go.k8s.io/owners + +labels: +- internal diff --git a/kustomize/OWNERS b/kustomize/OWNERS new file mode 100644 index 000000000..0846b3234 --- /dev/null +++ b/kustomize/OWNERS @@ -0,0 +1,4 @@ +# See the OWNERS docs at https://go.k8s.io/owners + +labels: +- kustomize diff --git a/pkg/apis/OWNERS b/pkg/apis/OWNERS new file mode 100644 index 000000000..16088024f --- /dev/null +++ b/pkg/apis/OWNERS @@ -0,0 +1,4 @@ +# See the OWNERS docs at https://go.k8s.io/owners + +labels: +- apis diff --git a/pkg/metrics/OWNERS b/pkg/metrics/OWNERS new file mode 100644 index 000000000..3191b8289 --- /dev/null +++ b/pkg/metrics/OWNERS @@ -0,0 +1,4 @@ +# See the OWNERS docs at https://go.k8s.io/owners + +labels: +- metrics diff --git a/pkg/rfc2317/OWNERS b/pkg/rfc2317/OWNERS new file mode 100644 index 000000000..593666f31 --- /dev/null +++ b/pkg/rfc2317/OWNERS @@ -0,0 +1,4 @@ +# See the OWNERS docs at https://go.k8s.io/owners + +labels: +- rfc2317 diff --git a/pkg/tlsutils/OWNERS b/pkg/tlsutils/OWNERS new file mode 100644 index 000000000..dc11b01c8 --- /dev/null +++ b/pkg/tlsutils/OWNERS @@ -0,0 +1,4 @@ +# See the OWNERS docs at https://go.k8s.io/owners + +labels: +- tls diff --git a/plan/OWNERS b/plan/OWNERS new file mode 100644 index 000000000..f660de545 --- /dev/null +++ b/plan/OWNERS @@ -0,0 +1,4 @@ +# See the OWNERS docs at https://go.k8s.io/owners + +labels: +- plan diff --git a/registry/OWNERS b/registry/OWNERS new file mode 100644 index 000000000..4de969042 --- /dev/null +++ b/registry/OWNERS @@ -0,0 +1,4 @@ +# See the OWNERS docs at https://go.k8s.io/owners + +labels: +- registry diff --git a/scripts/OWNERS b/scripts/OWNERS new file mode 100644 index 000000000..8f335dd0e --- /dev/null +++ b/scripts/OWNERS @@ -0,0 +1,4 @@ +# See the OWNERS docs at https://go.k8s.io/owners + +labels: +- scripts From 8cc73bd1e472dda2bd7eedaf068db117c80b24bd Mon Sep 17 00:00:00 2001 From: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com> Date: Thu, 3 Jul 2025 18:15:27 +0200 Subject: [PATCH 14/49] feat(traefik)!: disable legacy listeners on traefik.containo.us API Group (#5565) * feat(traefik)!: disable legacy listeners on traefik.containo.us API Group * update docs accordingly * update test accordingly * type argument is infered * fix rebase --- docs/flags.md | 2 +- docs/sources/traefik-proxy.md | 12 ++++------ pkg/apis/externaldns/types.go | 6 ++--- source/store.go | 6 ++--- source/traefik_proxy.go | 15 ++++++------ source/traefik_proxy_test.go | 43 +++++++++++++++-------------------- 6 files changed, 37 insertions(+), 47 deletions(-) diff --git a/docs/flags.md b/docs/flags.md index 14a045761..68f6307c8 100644 --- a/docs/flags.md +++ b/docs/flags.md @@ -50,7 +50,7 @@ | `--service-type-filter=SERVICE-TYPE-FILTER` | The service types to filter by. Specify multiple times for multiple filters to be applied. (optional, default: all, expected: ClusterIP, NodePort, LoadBalancer or ExternalName) | | `--source=source` | The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, node, pod, fake, connector, gateway-httproute, gateway-grpcroute, gateway-tlsroute, gateway-tcproute, gateway-udproute, istio-gateway, istio-virtualservice, cloudfoundry, contour-httpproxy, gloo-proxy, crd, empty, skipper-routegroup, openshift-route, ambassador-host, kong-tcpingress, f5-virtualserver, f5-transportserver, traefik-proxy) | | `--target-net-filter=TARGET-NET-FILTER` | Limit possible targets by a net filter; specify multiple times for multiple possible nets (optional) | -| `--[no-]traefik-disable-legacy` | Disable listeners on Resources under the traefik.containo.us API Group | +| `--[no-]traefik-enable-legacy` | Enable legacy listeners on Resources under the traefik.containo.us API Group | | `--[no-]traefik-disable-new` | Disable listeners on Resources under the traefik.io API Group | | `--provider=provider` | The DNS provider where the DNS records will be created (required, options: akamai, alibabacloud, aws, aws-sd, azure, azure-dns, azure-private-dns, civo, cloudflare, coredns, digitalocean, dnsimple, exoscale, gandi, godaddy, google, inmemory, linode, ns1, oci, ovh, pdns, pihole, plural, rfc2136, scaleway, skydns, transip, webhook) | | `--provider-cache-time=0s` | The time to cache the DNS provider record list requests. | diff --git a/docs/sources/traefik-proxy.md b/docs/sources/traefik-proxy.md index 20689a1ab..acb6f1489 100644 --- a/docs/sources/traefik-proxy.md +++ b/docs/sources/traefik-proxy.md @@ -82,15 +82,11 @@ kubectl delete -f externaldns.yaml | Flag | Description | |--------------------------|----------------------------------------------------------| -| --traefik-disable-legacy | Disable listeners on Resources under traefik.containo.us | +| --traefik-enable-legacy | Enable listeners on Resources under traefik.containo.us | | --traefik-disable-new | Disable listeners on Resources under traefik.io | -### Disabling Resource Listeners +### Resource Listeners -Traefik has deprecated the legacy API group, `traefik.containo.us`, in favor of `traefik.io`. By default the `traefik-proxy` source will listen for resources under both API groups; however, this may cause timeouts with the following message +Traefik has deprecated the legacy API group, _traefik.containo.us_, in favor of _traefik.io_. By default the `traefik-proxy` source listen for resources under traefik.io API groups. -```sh -FATA[0060] failed to sync traefik.io/v1alpha1, Resource=ingressroutes: context deadline exceeded -``` - -In this case you can disable one or the other API groups with `--traefik-disable-new` or `--traefik-disable-legacy` +If needed, you can enable legacy listener with `--traefik-enable-legacy` and also disable new listener with `--traefik-disable-new`. diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index fedfb8bd7..4b21a6a83 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -209,7 +209,7 @@ type Config struct { WebhookProviderReadTimeout time.Duration WebhookProviderWriteTimeout time.Duration WebhookServer bool - TraefikDisableLegacy bool + TraefikEnableLegacy bool TraefikDisableNew bool NAT64Networks []string ExcludeUnschedulable bool @@ -359,7 +359,7 @@ var defaultConfig = &Config{ TLSCA: "", TLSClientCert: "", TLSClientCertKey: "", - TraefikDisableLegacy: false, + TraefikEnableLegacy: false, TraefikDisableNew: false, TransIPAccountName: "", TransIPPrivateKeyFile: "", @@ -486,7 +486,7 @@ func App(cfg *Config) *kingpin.Application { app.Flag("service-type-filter", "The service types to filter by. Specify multiple times for multiple filters to be applied. (optional, default: all, expected: ClusterIP, NodePort, LoadBalancer or ExternalName)").Default(defaultConfig.ServiceTypeFilter...).StringsVar(&cfg.ServiceTypeFilter) app.Flag("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, node, pod, fake, connector, gateway-httproute, gateway-grpcroute, gateway-tlsroute, gateway-tcproute, gateway-udproute, istio-gateway, istio-virtualservice, cloudfoundry, contour-httpproxy, gloo-proxy, crd, empty, skipper-routegroup, openshift-route, ambassador-host, kong-tcpingress, f5-virtualserver, f5-transportserver, traefik-proxy)").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "node", "pod", "gateway-httproute", "gateway-grpcroute", "gateway-tlsroute", "gateway-tcproute", "gateway-udproute", "istio-gateway", "istio-virtualservice", "cloudfoundry", "contour-httpproxy", "gloo-proxy", "fake", "connector", "crd", "empty", "skipper-routegroup", "openshift-route", "ambassador-host", "kong-tcpingress", "f5-virtualserver", "f5-transportserver", "traefik-proxy") app.Flag("target-net-filter", "Limit possible targets by a net filter; specify multiple times for multiple possible nets (optional)").StringsVar(&cfg.TargetNetFilter) - app.Flag("traefik-disable-legacy", "Disable listeners on Resources under the traefik.containo.us API Group").Default(strconv.FormatBool(defaultConfig.TraefikDisableLegacy)).BoolVar(&cfg.TraefikDisableLegacy) + app.Flag("traefik-enable-legacy", "Enable legacy listeners on Resources under the traefik.containo.us API Group").Default(strconv.FormatBool(defaultConfig.TraefikEnableLegacy)).BoolVar(&cfg.TraefikEnableLegacy) app.Flag("traefik-disable-new", "Disable listeners on Resources under the traefik.io API Group").Default(strconv.FormatBool(defaultConfig.TraefikDisableNew)).BoolVar(&cfg.TraefikDisableNew) // Flags related to providers diff --git a/source/store.go b/source/store.go index cd458a0c8..579c3d574 100644 --- a/source/store.go +++ b/source/store.go @@ -96,7 +96,7 @@ type Config struct { OCPRouterName string UpdateEvents bool ResolveLoadBalancerHostname bool - TraefikDisableLegacy bool + TraefikEnableLegacy bool TraefikDisableNew bool ExcludeUnschedulable bool ExposeInternalIPv6 bool @@ -142,7 +142,7 @@ func NewSourceConfig(cfg *externaldns.Config) *Config { OCPRouterName: cfg.OCPRouterName, UpdateEvents: cfg.UpdateEvents, ResolveLoadBalancerHostname: cfg.ResolveServiceLoadBalancerHostname, - TraefikDisableLegacy: cfg.TraefikDisableLegacy, + TraefikEnableLegacy: cfg.TraefikEnableLegacy, TraefikDisableNew: cfg.TraefikDisableNew, ExcludeUnschedulable: cfg.ExcludeUnschedulable, ExposeInternalIPv6: cfg.ExposeInternalIPV6, @@ -533,7 +533,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.TraefikDisableLegacy, cfg.TraefikDisableNew) + return NewTraefikSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter, cfg.IgnoreHostnameAnnotation, cfg.TraefikEnableLegacy, cfg.TraefikDisableNew) } func buildOpenShiftRouteSource(ctx context.Context, p ClientGenerator, cfg *Config) (Source, error) { diff --git a/source/traefik_proxy.go b/source/traefik_proxy.go index 792efec24..af2ccd1b9 100644 --- a/source/traefik_proxy.go +++ b/source/traefik_proxy.go @@ -100,7 +100,8 @@ func NewTraefikSource( dynamicKubeClient dynamic.Interface, kubeClient kubernetes.Interface, namespace, annotationFilter string, - ignoreHostnameAnnotation, disableLegacy, disableNew bool) (Source, error) { + ignoreHostnameAnnotation, enableLegacy, disableNew bool, +) (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. informerFactory := dynamicinformer.NewFilteredDynamicSharedInformerFactory(dynamicKubeClient, 0, namespace, nil) @@ -128,7 +129,7 @@ func NewTraefikSource( }, ) } - if !disableLegacy { + if enableLegacy { oldIngressRouteInformer = informerFactory.ForResource(oldIngressRouteGVR) oldIngressRouteTcpInformer = informerFactory.ForResource(oldIngressRouteTCPGVR) oldIngressRouteUdpInformer = informerFactory.ForResource(oldIngressRouteUDPGVR) @@ -232,7 +233,7 @@ func (ts *traefikSource) Endpoints(_ context.Context) ([]*endpoint.Endpoint, err // ingressRouteEndpoints extracts endpoints from all IngressRoute objects func (ts *traefikSource) ingressRouteEndpoints() ([]*endpoint.Endpoint, error) { - return extractEndpoints[IngressRoute]( + return extractEndpoints( ts.ingressRouteInformer.Lister(), ts.namespace, func(u *unstructured.Unstructured) (*IngressRoute, error) { @@ -297,7 +298,7 @@ func (ts *traefikSource) ingressRouteTCPEndpoints() ([]*endpoint.Endpoint, error // ingressRouteUDPEndpoints extracts endpoints from all IngressRouteUDP objects func (ts *traefikSource) ingressRouteUDPEndpoints() ([]*endpoint.Endpoint, error) { - return extractEndpoints[IngressRouteUDP]( + return extractEndpoints( ts.ingressRouteUdpInformer.Lister(), ts.namespace, func(u *unstructured.Unstructured) (*IngressRouteUDP, error) { @@ -311,7 +312,7 @@ func (ts *traefikSource) ingressRouteUDPEndpoints() ([]*endpoint.Endpoint, error // oldIngressRouteEndpoints extracts endpoints from all IngressRoute objects func (ts *traefikSource) oldIngressRouteEndpoints() ([]*endpoint.Endpoint, error) { - return extractEndpoints[IngressRoute]( + return extractEndpoints( ts.oldIngressRouteInformer.Lister(), ts.namespace, func(u *unstructured.Unstructured) (*IngressRoute, error) { @@ -327,7 +328,7 @@ func (ts *traefikSource) oldIngressRouteEndpoints() ([]*endpoint.Endpoint, error // oldIngressRouteTCPEndpoints extracts endpoints from all IngressRouteTCP objects func (ts *traefikSource) oldIngressRouteTCPEndpoints() ([]*endpoint.Endpoint, error) { - return extractEndpoints[IngressRouteTCP]( + return extractEndpoints( ts.oldIngressRouteTcpInformer.Lister(), ts.namespace, func(u *unstructured.Unstructured) (*IngressRouteTCP, error) { @@ -341,7 +342,7 @@ func (ts *traefikSource) oldIngressRouteTCPEndpoints() ([]*endpoint.Endpoint, er // oldIngressRouteUDPEndpoints extracts endpoints from all IngressRouteUDP objects func (ts *traefikSource) oldIngressRouteUDPEndpoints() ([]*endpoint.Endpoint, error) { - return extractEndpoints[IngressRouteUDP]( + return extractEndpoints( ts.oldIngressRouteUdpInformer.Lister(), ts.namespace, func(u *unstructured.Unstructured) (*IngressRouteUDP, error) { diff --git a/source/traefik_proxy_test.go b/source/traefik_proxy_test.go index 9e345208a..abce58cbc 100644 --- a/source/traefik_proxy_test.go +++ b/source/traefik_proxy_test.go @@ -330,7 +330,6 @@ func TestTraefikProxyIngressRouteEndpoints(t *testing.T) { expected: nil, }, } { - t.Run(ti.title, func(t *testing.T) { t.Parallel() @@ -624,7 +623,6 @@ func TestTraefikProxyIngressRouteTCPEndpoints(t *testing.T) { expected: nil, }, } { - t.Run(ti.title, func(t *testing.T) { t.Parallel() @@ -766,7 +764,6 @@ func TestTraefikProxyIngressRouteUDPEndpoints(t *testing.T) { expected: nil, }, } { - t.Run(ti.title, func(t *testing.T) { t.Parallel() @@ -1096,7 +1093,6 @@ func TestTraefikProxyOldIngressRouteEndpoints(t *testing.T) { expected: nil, }, } { - t.Run(ti.title, func(t *testing.T) { t.Parallel() @@ -1121,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, false, false) + source, err := NewTraefikSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, defaultTraefikNamespace, "kubernetes.io/ingress.class=traefik", ti.ignoreHostnameAnnotation, true, false) assert.NoError(t, err) assert.NotNil(t, source) @@ -1390,7 +1386,6 @@ func TestTraefikProxyOldIngressRouteTCPEndpoints(t *testing.T) { expected: nil, }, } { - t.Run(ti.title, func(t *testing.T) { t.Parallel() @@ -1415,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, false, false) + source, err := NewTraefikSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, defaultTraefikNamespace, "kubernetes.io/ingress.class=traefik", ti.ignoreHostnameAnnotation, true, false) assert.NoError(t, err) assert.NotNil(t, source) @@ -1532,7 +1527,6 @@ func TestTraefikProxyOldIngressRouteUDPEndpoints(t *testing.T) { expected: nil, }, } { - t.Run(ti.title, func(t *testing.T) { t.Parallel() @@ -1557,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, false, false) + source, err := NewTraefikSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, defaultTraefikNamespace, "kubernetes.io/ingress.class=traefik", ti.ignoreHostnameAnnotation, true, false) assert.NoError(t, err) assert.NotNil(t, source) @@ -1574,7 +1568,7 @@ func TestTraefikProxyOldIngressRouteUDPEndpoints(t *testing.T) { } } -func TestTraefikAPIGroupDisableFlags(t *testing.T) { +func TestTraefikAPIGroupFlags(t *testing.T) { t.Parallel() for _, ti := range []struct { @@ -1582,7 +1576,7 @@ func TestTraefikAPIGroupDisableFlags(t *testing.T) { ingressRoute IngressRoute gvr schema.GroupVersionResource ignoreHostnameAnnotation bool - disableLegacy bool + enableLegacy bool disableNew bool expected []*endpoint.Endpoint }{ @@ -1603,9 +1597,9 @@ func TestTraefikAPIGroupDisableFlags(t *testing.T) { }, }, }, - gvr: oldIngressRouteGVR, - disableLegacy: false, - disableNew: false, + gvr: oldIngressRouteGVR, + enableLegacy: true, + disableNew: false, expected: []*endpoint.Endpoint{ { DNSName: "a.example.com", @@ -1636,9 +1630,9 @@ func TestTraefikAPIGroupDisableFlags(t *testing.T) { }, }, }, - gvr: oldIngressRouteGVR, - disableLegacy: true, - disableNew: false, + gvr: oldIngressRouteGVR, + enableLegacy: false, + disableNew: false, }, { title: "IngressRoute.traefik.io with the new API group enabled", @@ -1657,9 +1651,9 @@ func TestTraefikAPIGroupDisableFlags(t *testing.T) { }, }, }, - gvr: ingressRouteGVR, - disableLegacy: false, - disableNew: false, + gvr: ingressRouteGVR, + enableLegacy: true, + disableNew: false, expected: []*endpoint.Endpoint{ { DNSName: "a.example.com", @@ -1690,12 +1684,11 @@ func TestTraefikAPIGroupDisableFlags(t *testing.T) { }, }, }, - gvr: ingressRouteGVR, - disableLegacy: false, - disableNew: true, + gvr: ingressRouteGVR, + enableLegacy: true, + disableNew: true, }, } { - t.Run(ti.title, func(t *testing.T) { t.Parallel() @@ -1720,7 +1713,7 @@ func TestTraefikAPIGroupDisableFlags(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.disableLegacy, ti.disableNew) + source, err := NewTraefikSource(context.TODO(), fakeDynamicClient, fakeKubernetesClient, defaultTraefikNamespace, "kubernetes.io/ingress.class=traefik", ti.ignoreHostnameAnnotation, ti.enableLegacy, ti.disableNew) assert.NoError(t, err) assert.NotNil(t, source) From 4fd559660180661303ed801459d6c1abca4d2ff8 Mon Sep 17 00:00:00 2001 From: Ivan Ka <5395690+ivankatliarchuk@users.noreply.github.com> Date: Thu, 3 Jul 2025 17:15:34 +0100 Subject: [PATCH 15/49] feat(source/pods): support for annotation and label filter (#5583) * feat(source): pods added support for annotation filter and label selectors * feat(source/pods): support for annotation and label filter Signed-off-by: ivan katliarchuk --------- Signed-off-by: ivan katliarchuk --- docs/sources/about.md | 40 ++--- source/annotations/processors_test.go | 6 + source/informers/indexers.go | 113 +++++++++++++ source/informers/indexers_test.go | 185 ++++++++++++++++++++ source/pod.go | 27 ++- source/pod_fqdn_test.go | 12 +- source/pod_indexer_test.go | 232 ++++++++++++++++++++++++++ source/pod_test.go | 4 +- source/store.go | 2 +- 9 files changed, 588 insertions(+), 33 deletions(-) create mode 100644 source/informers/indexers.go create mode 100644 source/informers/indexers_test.go create mode 100644 source/pod_indexer_test.go diff --git a/docs/sources/about.md b/docs/sources/about.md index 1421b9a93..ea76edcac 100644 --- a/docs/sources/about.md +++ b/docs/sources/about.md @@ -5,26 +5,26 @@ A source in ExternalDNS defines where DNS records are discovered from within you ExternalDNS watches the specified sources for hostname information and uses it to create, update, or delete DNS records accordingly. Multiple sources can be configured simultaneously to support diverse environments. | Source | Resources | annotation-filter | label-filter | -| --------------------------------------- | ----------------------------------------------------------------------------- | ----------------- | ------------ | -| ambassador-host | Host.getambassador.io | Yes | Yes | +|-----------------------------------------|-------------------------------------------------------------------------------|:-----------------:|:------------:| +| ambassador-host | Host.getambassador.io | Yes | Yes | | connector | | | | -| contour-httpproxy | HttpProxy.projectcontour.io | Yes | | +| contour-httpproxy | HttpProxy.projectcontour.io | Yes | | | cloudfoundry | | | | -| [crd](crd.md) | DNSEndpoint.externaldns.k8s.io | Yes | Yes | -| [f5-virtualserver](f5-virtualserver.md) | VirtualServer.cis.f5.com | Yes | | -| [gateway-grpcroute](gateway.md) | GRPCRoute.gateway.networking.k8s.io | Yes | Yes | -| [gateway-httproute](gateway.md) | HTTPRoute.gateway.networking.k8s.io | Yes | Yes | -| [gateway-tcproute](gateway.md) | TCPRoute.gateway.networking.k8s.io | Yes | Yes | -| [gateway-tlsroute](gateway.md) | TLSRoute.gateway.networking.k8s.io | Yes | Yes | -| [gateway-udproute](gateway.md) | UDPRoute.gateway.networking.k8s.io | Yes | Yes | +| [crd](crd.md) | DNSEndpoint.externaldns.k8s.io | Yes | Yes | +| [f5-virtualserver](f5-virtualserver.md) | VirtualServer.cis.f5.com | Yes | | +| [gateway-grpcroute](gateway.md) | GRPCRoute.gateway.networking.k8s.io | Yes | Yes | +| [gateway-httproute](gateway.md) | HTTPRoute.gateway.networking.k8s.io | Yes | Yes | +| [gateway-tcproute](gateway.md) | TCPRoute.gateway.networking.k8s.io | Yes | Yes | +| [gateway-tlsroute](gateway.md) | TLSRoute.gateway.networking.k8s.io | Yes | Yes | +| [gateway-udproute](gateway.md) | UDPRoute.gateway.networking.k8s.io | Yes | Yes | | [gloo-proxy](gloo-proxy.md) | Proxy.gloo.solo.io | | | -| [ingress](ingress.md) | Ingress.networking.k8s.io | Yes | Yes | -| [istio-gateway](istio.md) | Gateway.networking.istio.io | Yes | | -| [istio-virtualservice](istio.md) | VirtualService.networking.istio.io | Yes | | -| [kong-tcpingress](kong.md) | TCPIngress.configuration.konghq.com | Yes | | -| [node](nodes.md) | Node | Yes | Yes | -| [openshift-route](openshift.md) | Route.route.openshift.io | Yes | Yes | -| [pod](pod.md) | Pod | | | -| [service](service.md) | Service | Yes | Yes | -| skipper-routegroup | RouteGroup.zalando.org | Yes | | -| [traefik-proxy](traefik-proxy.md) | IngressRoute.traefik.io IngressRouteTCP.traefik.io IngressRouteUDP.traefik.io | Yes | | +| [ingress](ingress.md) | Ingress.networking.k8s.io | Yes | Yes | +| [istio-gateway](istio.md) | Gateway.networking.istio.io | Yes | | +| [istio-virtualservice](istio.md) | VirtualService.networking.istio.io | Yes | | +| [kong-tcpingress](kong.md) | TCPIngress.configuration.konghq.com | Yes | | +| [node](nodes.md) | Node | Yes | Yes | +| [openshift-route](openshift.md) | Route.route.openshift.io | Yes | Yes | +| [pod](pod.md) | Pod | Yes | Yes | +| [service](service.md) | Service | Yes | Yes | +| skipper-routegroup | RouteGroup.zalando.org | Yes | | +| [traefik-proxy](traefik-proxy.md) | IngressRoute.traefik.io IngressRouteTCP.traefik.io IngressRouteUDP.traefik.io | Yes | | diff --git a/source/annotations/processors_test.go b/source/annotations/processors_test.go index d423edc71..c056f52a3 100644 --- a/source/annotations/processors_test.go +++ b/source/annotations/processors_test.go @@ -47,6 +47,12 @@ func TestParseAnnotationFilter(t *testing.T) { expectedSelector: labels.Set{}.AsSelector(), expectError: false, }, + { + name: "wrong annotation filter", + annotationFilter: "=test", + expectedSelector: nil, + expectError: true, + }, } for _, tt := range tests { diff --git a/source/informers/indexers.go b/source/informers/indexers.go new file mode 100644 index 000000000..b2b947da4 --- /dev/null +++ b/source/informers/indexers.go @@ -0,0 +1,113 @@ +/* +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 informers + +import ( + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/cache" + + "sigs.k8s.io/external-dns/source/annotations" +) + +const ( + IndexWithSelectors = "withSelectors" +) + +type IndexSelectorOptions struct { + annotationFilter labels.Selector + labelSelector labels.Selector +} + +func IndexSelectorWithAnnotationFilter(input string) func(options *IndexSelectorOptions) { + return func(options *IndexSelectorOptions) { + if input == "" { + return + } + selector, err := annotations.ParseFilter(input) + if err != nil { + return + } + options.annotationFilter = selector + } +} + +func IndexSelectorWithLabelSelector(input labels.Selector) func(options *IndexSelectorOptions) { + return func(options *IndexSelectorOptions) { + options.labelSelector = input + } +} + +// IndexerWithOptions is a generic function that allows adding multiple indexers +// to a SharedIndexInformer for a specific Kubernetes resource type T. It accepts +// a variadic list of indexer functions, which define custom indexing logic. +// +// Each indexer function is applied to objects of type T, enabling flexible and +// reusable indexing based on annotations, labels, or other criteria. +// +// Example usage: +// err := IndexerWithOptions[*v1.Pod]( +// +// IndexSelectorWithAnnotationFilter("example-annotation"), +// IndexSelectorWithLabelSelector(labels.SelectorFromSet(labels.Set{"app": "my-app"})), +// +// ) +// +// This function ensures type safety and simplifies the process of adding +// custom indexers to informers. +func IndexerWithOptions[T metav1.Object](optFns ...func(options *IndexSelectorOptions)) cache.Indexers { + options := IndexSelectorOptions{} + for _, fn := range optFns { + fn(&options) + } + + return cache.Indexers{ + IndexWithSelectors: func(obj interface{}) ([]string, error) { + entity, ok := obj.(T) + if !ok { + return nil, fmt.Errorf("object is not of type %T", new(T)) + } + + if options.annotationFilter != nil && !options.annotationFilter.Matches(labels.Set(entity.GetAnnotations())) { + return nil, nil + } + + if options.labelSelector != nil && !options.labelSelector.Matches(labels.Set(entity.GetLabels())) { + return nil, nil + } + key := types.NamespacedName{Namespace: entity.GetNamespace(), Name: entity.GetName()}.String() + return []string{key}, nil + }, + } +} + +// GetByKey retrieves an object of type T (metav1.Object) from the given cache.Indexer by its key. +// It returns the object and an error if the retrieval or type assertion fails. +// If the object does not exist, it returns the zero value of T and nil. +func GetByKey[T metav1.Object](indexer cache.Indexer, key string) (T, error) { + var entity T + obj, exists, err := indexer.GetByKey(key) + if err != nil || !exists { + return entity, err + } + + entity, ok := obj.(T) + if !ok { + return entity, fmt.Errorf("object is not of type %T", new(T)) + } + return entity, nil +} diff --git a/source/informers/indexers_test.go b/source/informers/indexers_test.go new file mode 100644 index 000000000..d9b5de61e --- /dev/null +++ b/source/informers/indexers_test.go @@ -0,0 +1,185 @@ +/* +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 informers + +import ( + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" + "sigs.k8s.io/external-dns/source/annotations" +) + +func TestIndexerWithOptions_FilterByAnnotation(t *testing.T) { + indexers := IndexerWithOptions[*unstructured.Unstructured]( + IndexSelectorWithAnnotationFilter("example-annotation"), + ) + + obj := &unstructured.Unstructured{} + obj.SetAnnotations(map[string]string{"example-annotation": "value"}) + obj.SetNamespace("default") + obj.SetName("test-object") + + keys, err := indexers[IndexWithSelectors](obj) + assert.NoError(t, err) + assert.Equal(t, []string{"default/test-object"}, keys) +} + +func TestIndexerWithOptions_FilterByLabel(t *testing.T) { + labelSelector := labels.SelectorFromSet(labels.Set{"app": "nginx"}) + indexers := IndexerWithOptions[*corev1.Pod]( + IndexSelectorWithLabelSelector(labelSelector), + ) + + obj := &corev1.Pod{} + obj.SetLabels(map[string]string{"app": "nginx"}) + obj.SetNamespace("default") + obj.SetName("test-object") + + keys, err := indexers[IndexWithSelectors](obj) + assert.NoError(t, err) + assert.Equal(t, []string{"default/test-object"}, keys) +} + +func TestIndexerWithOptions_NoMatch(t *testing.T) { + labelSelector := labels.SelectorFromSet(labels.Set{"app": "nginx"}) + indexers := IndexerWithOptions[*unstructured.Unstructured]( + IndexSelectorWithLabelSelector(labelSelector), + ) + + obj := &unstructured.Unstructured{} + obj.SetLabels(map[string]string{"app": "apache"}) + obj.SetNamespace("default") + obj.SetName("test-object") + + keys, err := indexers[IndexWithSelectors](obj) + assert.NoError(t, err) + assert.Nil(t, keys) +} + +func TestIndexerWithOptions_InvalidType(t *testing.T) { + indexers := IndexerWithOptions[*unstructured.Unstructured]() + + obj := "invalid-object" + + keys, err := indexers[IndexWithSelectors](obj) + assert.Error(t, err) + assert.Nil(t, keys) + assert.Contains(t, err.Error(), "object is not of type") +} + +func TestIndexerWithOptions_EmptyOptions(t *testing.T) { + indexers := IndexerWithOptions[*unstructured.Unstructured]() + + obj := &unstructured.Unstructured{} + obj.SetNamespace("default") + obj.SetName("test-object") + + keys, err := indexers["withSelectors"](obj) + assert.NoError(t, err) + assert.Equal(t, []string{"default/test-object"}, keys) +} + +func TestIndexerWithOptions_AnnotationFilterNoMatch(t *testing.T) { + indexers := IndexerWithOptions[*unstructured.Unstructured]( + IndexSelectorWithAnnotationFilter("example-annotation=value"), + ) + + obj := &unstructured.Unstructured{} + obj.SetAnnotations(map[string]string{"other-annotation": "value"}) + obj.SetNamespace("default") + obj.SetName("test-object") + + keys, err := indexers[IndexWithSelectors](obj) + assert.NoError(t, err) + assert.Nil(t, keys) +} + +func TestIndexSelectorWithAnnotationFilter(t *testing.T) { + tests := []struct { + name string + input string + expectedFilter labels.Selector + }{ + { + name: "valid input", + input: "key=value", + expectedFilter: func() labels.Selector { s, _ := annotations.ParseFilter("key=value"); return s }(), + }, + { + name: "empty input", + input: "", + expectedFilter: nil, + }, + { + name: "key only filter", + input: "app", + expectedFilter: func() labels.Selector { s, _ := annotations.ParseFilter("app"); return s }(), + }, + { + name: "poisoned input", + input: "=app", + expectedFilter: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + options := &IndexSelectorOptions{} + IndexSelectorWithAnnotationFilter(tt.input)(options) + assert.Equal(t, tt.expectedFilter, options.annotationFilter) + }) + } +} + +func TestGetByKey_ObjectExists(t *testing.T) { + indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{}) + pod := &corev1.Pod{} + pod.SetNamespace("default") + pod.SetName("test-pod") + + err := indexer.Add(pod) + assert.NoError(t, err) + + result, err := GetByKey[*corev1.Pod](indexer, "default/test-pod") + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, "test-pod", result.GetName()) +} + +func TestGetByKey_ObjectDoesNotExist(t *testing.T) { + indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{}) + + result, err := GetByKey[*corev1.Pod](indexer, "default/non-existent-pod") + assert.NoError(t, err) + assert.Nil(t, result) +} + +func TestGetByKey_TypeAssertionFailure(t *testing.T) { + indexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{}) + service := &corev1.Service{} + service.SetNamespace("default") + service.SetName("test-service") + + err := indexer.Add(service) + assert.NoError(t, err) + + result, err := GetByKey[*corev1.Pod](indexer, "default/test-service") + assert.Error(t, err) + assert.Contains(t, err.Error(), "object is not of type") + assert.Nil(t, result) +} diff --git a/source/pod.go b/source/pod.go index 9a52653c0..25597fdeb 100644 --- a/source/pod.go +++ b/source/pod.go @@ -25,15 +25,15 @@ import ( log "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/labels" + kubeinformers "k8s.io/client-go/informers" coreinformers "k8s.io/client-go/informers/core/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/cache" - "sigs.k8s.io/external-dns/source/fqdn" - "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/source/annotations" + "sigs.k8s.io/external-dns/source/fqdn" "sigs.k8s.io/external-dns/source/informers" ) @@ -60,11 +60,22 @@ func NewPodSource( podSourceDomain string, fqdnTemplate string, combineFqdnAnnotation bool, + annotationFilter string, + labelSelector labels.Selector, ) (Source, error) { informerFactory := kubeinformers.NewSharedInformerFactoryWithOptions(kubeClient, 0, kubeinformers.WithNamespace(namespace)) podInformer := informerFactory.Core().V1().Pods() nodeInformer := informerFactory.Core().V1().Nodes() + err := podInformer.Informer().AddIndexers(informers.IndexerWithOptions[*corev1.Pod]( + informers.IndexSelectorWithAnnotationFilter(annotationFilter), + informers.IndexSelectorWithLabelSelector(labelSelector), + )) + + if err != nil { + return nil, fmt.Errorf("failed to add indexers to pod informer: %w", err) + } + _, _ = podInformer.Informer().AddEventHandler( cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { @@ -107,13 +118,15 @@ func (*podSource) AddEventHandler(_ context.Context, _ func()) { } func (ps *podSource) Endpoints(_ context.Context) ([]*endpoint.Endpoint, error) { - pods, err := ps.podInformer.Lister().Pods(ps.namespace).List(labels.Everything()) - if err != nil { - return nil, err - } + indexKeys := ps.podInformer.Informer().GetIndexer().ListIndexFuncValues(informers.IndexWithSelectors) endpointMap := make(map[endpoint.EndpointKey][]string) - for _, pod := range pods { + for _, key := range indexKeys { + pod, err := informers.GetByKey[*corev1.Pod](ps.podInformer.Informer().GetIndexer(), key) + if err != nil { + continue + } + if ps.fqdnTemplate == nil || ps.combineFQDNAnnotation { ps.addPodEndpointsToEndpointMap(endpointMap, pod) } diff --git a/source/pod_fqdn_test.go b/source/pod_fqdn_test.go index 28f656220..3343fcba0 100644 --- a/source/pod_fqdn_test.go +++ b/source/pod_fqdn_test.go @@ -58,7 +58,9 @@ func TestNewPodSourceWithFqdn(t *testing.T) { false, "", tt.fqdnTemplate, - false) + false, + "", + nil) if tt.expectError { assert.Error(t, err) @@ -405,7 +407,9 @@ func TestPodSourceFqdnTemplatingExamples(t *testing.T) { false, tt.sourceDomain, tt.fqdnTemplate, - tt.combineFQDN) + tt.combineFQDN, + "", + nil) require.NoError(t, err) endpoints, err := src.Endpoints(t.Context()) @@ -467,7 +471,9 @@ func TestPodSourceFqdnTemplatingExamples_Failed(t *testing.T) { false, tt.sourceDomain, tt.fqdnTemplate, - tt.combineFQDN) + tt.combineFQDN, + "", + nil) require.NoError(t, err) _, err = src.Endpoints(t.Context()) diff --git a/source/pod_indexer_test.go b/source/pod_indexer_test.go new file mode 100644 index 000000000..402337a39 --- /dev/null +++ b/source/pod_indexer_test.go @@ -0,0 +1,232 @@ +/* +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 ( + "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" + "sigs.k8s.io/external-dns/source/annotations" +) + +type podSpec struct { + namespace string + labels map[string]string + annotations map[string]string + // with labels and annotations + totalTarget int + // without provided labels and annotations + totalRandom int +} + +func fixtureCreatePodsWithNodes(input []podSpec) []*corev1.Pod { + var pods []*corev1.Pod + + var createPod = func(index int, spec podSpec) *corev1.Pod { + return &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("pod-%d-%s", index, uuid.NewString()), + Namespace: spec.namespace, + Labels: func() map[string]string { + if spec.totalTarget > index { + return spec.labels + } + return map[string]string{ + "app": fmt.Sprintf("my-app-%d", rand.IntN(10)), + "index": strconv.Itoa(index), + } + }(), + Annotations: func() map[string]string { + if spec.totalTarget > index { + return spec.annotations + } + return map[string]string{ + "key1": fmt.Sprintf("value-%d", rand.IntN(10)), + } + }(), + }, + Spec: corev1.PodSpec{}, + Status: corev1.PodStatus{ + Phase: corev1.PodRunning, + PodIPs: []corev1.PodIP{ + {IP: net.IPv4(192, byte(rand.IntN(250)), byte(rand.IntN(250)), byte(index)).String()}, + }, + }, + } + } + + for _, el := range input { + totalPods := el.totalTarget + el.totalRandom + for i := 0; i < totalPods; i++ { + pods = append(pods, createPod(i, el)) + } + } + + for i := 0; i < 3; i++ { + rand.Shuffle(len(pods), func(i, j int) { + pods[i], pods[j] = pods[j], pods[i] + }) + } + // assign nodes to pods + for i, pod := range pods { + pod.Spec.NodeName = fmt.Sprintf("node-%d", i/5) // Assign 5 pods per node + } + return pods +} + +func TestPodsWithAnnotationsAndLabels(t *testing.T) { + // total target pods 700 + // total random pods 3950 + pods := fixtureCreatePodsWithNodes([]podSpec{ + { + namespace: "dev", + labels: map[string]string{"app": "nginx", "env": "dev", "agent": "enabled"}, + annotations: map[string]string{"arch": "amd64"}, + totalTarget: 300, + totalRandom: 700, + }, + { + namespace: "prod", + labels: map[string]string{"app": "nginx", "env": "prod", "agent": "enabled"}, + annotations: map[string]string{"arch": "amd64"}, + totalTarget: 150, + totalRandom: 2700, + }, + { + namespace: "default", + labels: map[string]string{"app": "nginx", "agent": "disabled"}, + annotations: map[string]string{"arch": "amd64"}, + totalTarget: 250, + totalRandom: 450, + }, + { + namespace: "kube-system", + labels: map[string]string{}, + annotations: map[string]string{}, + totalTarget: 0, + totalRandom: 100, + }, + }) + + client := fake.NewClientset() + + nodes := map[string]bool{} + + for _, pod := range pods { + if _, exists := nodes[pod.Spec.NodeName]; !exists { + nodes[pod.Spec.NodeName] = true + node := &corev1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: pod.Spec.NodeName, + }, + } + if _, err := client.CoreV1().Nodes().Create(t.Context(), node, metav1.CreateOptions{}); err != nil { + assert.NoError(t, err) + } + } + if _, err := client.CoreV1().Pods(pod.Namespace).Create(t.Context(), pod, metav1.CreateOptions{}); err != nil { + assert.NoError(t, err) + } + } + + tests := []struct { + name string + namespace string + labelSelector string + annotationFilter string + expectedEndpointCount int + }{ + { + name: "prod namespace with labels", + namespace: "prod", + labelSelector: "app=nginx", + expectedEndpointCount: 150, + }, + { + name: "prod namespace with annotations", + namespace: "prod", + annotationFilter: "arch=amd64", + expectedEndpointCount: 150, + }, + { + name: "prod namespace with annotations and labels not exists", + namespace: "prod", + labelSelector: "app=not-exists", + annotationFilter: "arch=amd64", + expectedEndpointCount: 0, + }, + { + name: "all namespaces with correct annotations and labels", + namespace: "", + labelSelector: "app=nginx,agent=enabled", + annotationFilter: "arch=amd64", + expectedEndpointCount: 450, // 300 from dev + 150 from prod + }, + { + name: "all namespaces with loose annotations and labels", + namespace: "", + labelSelector: "app=nginx", + annotationFilter: "arch=amd64", + expectedEndpointCount: 700, // 300 from dev + 150 from prod + 250 from default + }, + { + name: "all namespaces with loose annotations and labels", + namespace: "", + labelSelector: "agent", + annotationFilter: "arch", + expectedEndpointCount: 700, + }, + { + name: "all namespaces without filters", + namespace: "", + labelSelector: "", + annotationFilter: "", + expectedEndpointCount: 4650, + }, + { + name: "single namespace without filters", + namespace: "default", + labelSelector: "", + annotationFilter: "", + expectedEndpointCount: 700, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + selector, _ := annotations.ParseFilter(tt.labelSelector) + pSource, err := NewPodSource( + t.Context(), client, + tt.namespace, "", + false, "", + "{{ .Name }}.tld.org", false, + tt.annotationFilter, selector) + require.NoError(t, err) + + endpoints, err := pSource.Endpoints(t.Context()) + require.NoError(t, err) + + assert.Len(t, endpoints, tt.expectedEndpointCount) + }) + } +} diff --git a/source/pod_test.go b/source/pod_test.go index 40592279e..f2d67da5a 100644 --- a/source/pod_test.go +++ b/source/pod_test.go @@ -657,7 +657,7 @@ func TestPodSource(t *testing.T) { } } - client, err := NewPodSource(ctx, kubernetes, tc.targetNamespace, tc.compatibility, tc.ignoreNonHostNetworkPods, tc.PodSourceDomain, "", false) + client, err := NewPodSource(ctx, kubernetes, tc.targetNamespace, tc.compatibility, tc.ignoreNonHostNetworkPods, tc.PodSourceDomain, "", false, "", nil) require.NoError(t, err) endpoints, err := client.Endpoints(ctx) @@ -885,7 +885,7 @@ func TestPodSourceLogs(t *testing.T) { } } - client, err := NewPodSource(ctx, kubernetes, "", "", tc.ignoreNonHostNetworkPods, "", "", false) + client, err := NewPodSource(ctx, kubernetes, "", "", tc.ignoreNonHostNetworkPods, "", "", false, "", nil) require.NoError(t, err) hook := testutils.LogsUnderTestWithLogLevel(log.DebugLevel, t) diff --git a/source/store.go b/source/store.go index 579c3d574..8d69a31bb 100644 --- a/source/store.go +++ b/source/store.go @@ -448,7 +448,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) + return NewPodSource(ctx, client, cfg.Namespace, cfg.Compatibility, cfg.IgnoreNonHostNetworkPods, cfg.PodSourceDomain, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.AnnotationFilter, cfg.LabelFilter) } // buildIstioGatewaySource creates an Istio Gateway source for exposing Istio gateways as DNS records. From 65553c6593e84d94600141062e8338a890ece544 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Jul 2025 00:17:26 -0700 Subject: [PATCH 16/49] chore(deps): bump the dev-dependencies group with 2 updates (#5610) Bumps the dev-dependencies group with 2 updates: [github.com/aws/aws-sdk-go-v2/service/route53](https://github.com/aws/aws-sdk-go-v2) and [github.com/linode/linodego](https://github.com/linode/linodego). Updates `github.com/aws/aws-sdk-go-v2/service/route53` from 1.52.2 to 1.53.0 - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/changelog-template.json) - [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/ecs/v1.52.2...service/s3/v1.53.0) Updates `github.com/linode/linodego` from 1.52.2 to 1.53.0 - [Release notes](https://github.com/linode/linodego/releases) - [Commits](https://github.com/linode/linodego/compare/v1.52.2...v1.53.0) --- updated-dependencies: - dependency-name: github.com/aws/aws-sdk-go-v2/service/route53 dependency-version: 1.53.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: github.com/linode/linodego dependency-version: 1.53.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dev-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index c18f444cb..05e9dae5f 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/aws/aws-sdk-go-v2/credentials v1.17.70 github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.4 github.com/aws/aws-sdk-go-v2/service/dynamodb v1.44.0 - github.com/aws/aws-sdk-go-v2/service/route53 v1.52.2 + github.com/aws/aws-sdk-go-v2/service/route53 v1.53.0 github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.35.7 github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 github.com/bodgit/tsig v1.2.2 @@ -38,7 +38,7 @@ require ( github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 github.com/linki/instrumented_http v0.3.0 - github.com/linode/linodego v1.52.2 + github.com/linode/linodego v1.53.0 github.com/maxatome/go-testdeep v1.14.0 github.com/miekg/dns v1.1.66 github.com/openshift/api v0.0.0-20230607130528-611114dca681 diff --git a/go.sum b/go.sum index a75ee74e6..6a5c97361 100644 --- a/go.sum +++ b/go.sum @@ -140,8 +140,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.17 h1:x18 github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.17/go.mod h1:mC9qMbA6e1pwEq6X3zDGtZRXMG2YaElJkbJlMVHLs5I= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 h1:t0E6FzREdtCsiLIoLCWsYliNsRBgyGD/MCK571qk4MI= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17/go.mod h1:ygpklyoaypuyDvOM5ujWGrYWpAK3h7ugnmKCU/76Ys4= -github.com/aws/aws-sdk-go-v2/service/route53 v1.52.2 h1:dXHWVVPx2W2fq2PTugj8QXpJ0YTRAGx0KLPKhMBmcsY= -github.com/aws/aws-sdk-go-v2/service/route53 v1.52.2/go.mod h1:wi1naoiPnCQG3cyjsivwPON1ZmQt/EJGxFqXzubBTAw= +github.com/aws/aws-sdk-go-v2/service/route53 v1.53.0 h1:UglIEyurCqfzZkjNdYAuXUGFu/FNWMKP5eorzggvXe8= +github.com/aws/aws-sdk-go-v2/service/route53 v1.53.0/go.mod h1:wi1naoiPnCQG3cyjsivwPON1ZmQt/EJGxFqXzubBTAw= github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.35.7 h1:1eaP4/444jrv04HhJdwTHtgnyxWgxwdLjSYBGq+oMB4= github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.35.7/go.mod h1:czoZQabc2chvmV/ak4oGSNR9CbcUw2bef3tatmwtoIA= github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 h1:AIRJ3lfb2w/1/8wOOSqYb9fUKGwQbtysJ2H1MofRUPg= @@ -676,8 +676,8 @@ github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-b github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/linki/instrumented_http v0.3.0 h1:dsN92+mXpfZtjJraartcQ99jnuw7fqsnPDjr85ma2dA= github.com/linki/instrumented_http v0.3.0/go.mod h1:pjYbItoegfuVi2GUOMhEqzvm/SJKuEL3H0tc8QRLRFk= -github.com/linode/linodego v1.52.2 h1:N9ozU27To1LMSrDd8WvJZ5STSz1eGYdyLnxhAR/dIZg= -github.com/linode/linodego v1.52.2/go.mod h1:bI949fZaVchjWyKIA08hNyvAcV6BAS+PM2op3p7PAWA= +github.com/linode/linodego v1.53.0 h1:UWr7bUUVMtcfsuapC+6blm6+jJLPd7Tf9MZUpdOERnI= +github.com/linode/linodego v1.53.0/go.mod h1:bI949fZaVchjWyKIA08hNyvAcV6BAS+PM2op3p7PAWA= github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= github.com/lyft/protoc-gen-star v0.4.10/go.mod h1:mE8fbna26u7aEA2QCVvvfBU/ZrPgocG1206xAFPcs94= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= From 1b77c19d3c5cb947ea52e2ec0ae3b7f9597a385b Mon Sep 17 00:00:00 2001 From: Ivan Ka <5395690+ivankatliarchuk@users.noreply.github.com> Date: Sun, 6 Jul 2025 09:17:25 +0100 Subject: [PATCH 17/49] chore(codebase): reuse functions (#5607) Signed-off-by: ivan katliarchuk --- source/ambassador_host.go | 10 ++-------- source/contour_httpproxy.go | 13 ++++--------- source/endpoints.go | 15 ++------------- source/endpoints_test.go | 2 +- source/f5_transportserver.go | 10 ++-------- source/f5_virtualserver.go | 10 ++-------- source/gateway.go | 2 +- source/gloo_proxy.go | 2 +- source/ingress.go | 8 ++++---- source/istio_gateway.go | 2 +- source/istio_virtualservice.go | 6 +++--- source/kong_tcpingress.go | 4 ++-- source/openshift_route.go | 6 +++--- source/service.go | 15 +++------------ source/skipper_routegroup.go | 6 +++--- source/traefik_proxy.go | 16 ++++++---------- 16 files changed, 40 insertions(+), 87 deletions(-) diff --git a/source/ambassador_host.go b/source/ambassador_host.go index d126c2941..6b1da8195 100644 --- a/source/ambassador_host.go +++ b/source/ambassador_host.go @@ -196,7 +196,7 @@ func (sc *ambassadorHostSource) endpointsFromHost(host *ambassador.Host, targets if host.Spec != nil { hostname := host.Spec.Hostname if hostname != "" { - endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) + endpoints = append(endpoints, EndpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) } } @@ -294,13 +294,7 @@ func newUnstructuredConverter() (*unstructuredConverter, error) { // Filter a list of Ambassador Host Resources to only return the ones that // contain the required External-DNS annotation filter func (sc *ambassadorHostSource) filterByAnnotations(ambassadorHosts []*ambassador.Host) ([]*ambassador.Host, error) { - // External-DNS Annotation Filter - labelSelector, err := metav1.ParseToLabelSelector(sc.annotationFilter) - if err != nil { - return nil, err - } - - selector, err := metav1.LabelSelectorAsSelector(labelSelector) + selector, err := annotations.ParseFilter(sc.annotationFilter) if err != nil { return nil, err } diff --git a/source/contour_httpproxy.go b/source/contour_httpproxy.go index b0a57171c..f530ee008 100644 --- a/source/contour_httpproxy.go +++ b/source/contour_httpproxy.go @@ -25,7 +25,6 @@ import ( projectcontour "github.com/projectcontour/contour/apis/projectcontour/v1" log "github.com/sirupsen/logrus" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" "k8s.io/client-go/dynamic" @@ -205,18 +204,14 @@ func (sc *httpProxySource) endpointsFromTemplate(httpProxy *projectcontour.HTTPP var endpoints []*endpoint.Endpoint for _, hostname := range hostnames { - endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) + endpoints = append(endpoints, EndpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) } return endpoints, nil } // filterByAnnotations filters a list of configs by a given annotation selector. func (sc *httpProxySource) filterByAnnotations(httpProxies []*projectcontour.HTTPProxy) ([]*projectcontour.HTTPProxy, error) { - labelSelector, err := metav1.ParseToLabelSelector(sc.annotationFilter) - if err != nil { - return nil, err - } - selector, err := metav1.LabelSelectorAsSelector(labelSelector) + selector, err := annotations.ParseFilter(sc.annotationFilter) if err != nil { return nil, err } @@ -263,7 +258,7 @@ func (sc *httpProxySource) endpointsFromHTTPProxy(httpProxy *projectcontour.HTTP if virtualHost := httpProxy.Spec.VirtualHost; virtualHost != nil { if fqdn := virtualHost.Fqdn; fqdn != "" { - endpoints = append(endpoints, endpointsForHostname(fqdn, targets, ttl, providerSpecific, setIdentifier, resource)...) + endpoints = append(endpoints, EndpointsForHostname(fqdn, targets, ttl, providerSpecific, setIdentifier, resource)...) } } @@ -271,7 +266,7 @@ func (sc *httpProxySource) endpointsFromHTTPProxy(httpProxy *projectcontour.HTTP if !sc.ignoreHostnameAnnotation { hostnameList := annotations.HostnamesFromAnnotations(httpProxy.Annotations) for _, hostname := range hostnameList { - endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) + endpoints = append(endpoints, EndpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) } } diff --git a/source/endpoints.go b/source/endpoints.go index 772c1b9ce..b3667052d 100644 --- a/source/endpoints.go +++ b/source/endpoints.go @@ -22,8 +22,8 @@ import ( "sigs.k8s.io/external-dns/endpoint" ) -// endpointsForHostname returns the endpoint objects for each host-target combination. -func endpointsForHostname(hostname string, targets endpoint.Targets, ttl endpoint.TTL, providerSpecific endpoint.ProviderSpecific, setIdentifier string, resource string) []*endpoint.Endpoint { +// EndpointsForHostname returns the endpoint objects for each host-target combination. +func EndpointsForHostname(hostname string, targets endpoint.Targets, ttl endpoint.TTL, providerSpecific endpoint.ProviderSpecific, setIdentifier string, resource string) []*endpoint.Endpoint { var ( endpoints []*endpoint.Endpoint aTargets endpoint.Targets @@ -81,17 +81,6 @@ func endpointsForHostname(hostname string, targets endpoint.Targets, ttl endpoin return endpoints } -func EndpointsForHostname( - hostname string, - targets endpoint.Targets, - ttl endpoint.TTL, - providerSpecific endpoint.ProviderSpecific, - setIdentifier string, - resource string, -) []*endpoint.Endpoint { - return endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource) -} - func EndpointTargetsFromServices(svcInformer coreinformers.ServiceInformer, namespace string, selector map[string]string) (endpoint.Targets, error) { targets := endpoint.Targets{} diff --git a/source/endpoints_test.go b/source/endpoints_test.go index dcd3571d7..9470db689 100644 --- a/source/endpoints_test.go +++ b/source/endpoints_test.go @@ -117,7 +117,7 @@ func TestEndpointsForHostname(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := endpointsForHostname(tt.hostname, tt.targets, tt.ttl, tt.providerSpecific, tt.setIdentifier, tt.resource) + result := EndpointsForHostname(tt.hostname, tt.targets, tt.ttl, tt.providerSpecific, tt.setIdentifier, tt.resource) assert.Equal(t, tt.expected, result) }) } diff --git a/source/f5_transportserver.go b/source/f5_transportserver.go index 6b08f6288..8061bd8b3 100644 --- a/source/f5_transportserver.go +++ b/source/f5_transportserver.go @@ -23,7 +23,6 @@ import ( "strings" log "github.com/sirupsen/logrus" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" @@ -163,7 +162,7 @@ func (ts *f5TransportServerSource) endpointsFromTransportServers(transportServer targets = append(targets, transportServer.Status.VSAddress) } - endpoints = append(endpoints, endpointsForHostname(transportServer.Spec.Host, targets, ttl, nil, "", resource)...) + endpoints = append(endpoints, EndpointsForHostname(transportServer.Spec.Host, targets, ttl, nil, "", resource)...) } return endpoints, nil @@ -186,12 +185,7 @@ func newTSUnstructuredConverter() (*unstructuredConverter, error) { // filterByAnnotations filters a list of TransportServers by a given annotation selector. func (ts *f5TransportServerSource) filterByAnnotations(transportServers []*f5.TransportServer) ([]*f5.TransportServer, error) { - labelSelector, err := metav1.ParseToLabelSelector(ts.annotationFilter) - if err != nil { - return nil, err - } - - selector, err := metav1.LabelSelectorAsSelector(labelSelector) + selector, err := annotations.ParseFilter(ts.annotationFilter) if err != nil { return nil, err } diff --git a/source/f5_virtualserver.go b/source/f5_virtualserver.go index 7d28f6e7b..a97baa9e0 100644 --- a/source/f5_virtualserver.go +++ b/source/f5_virtualserver.go @@ -24,7 +24,6 @@ import ( "strings" log "github.com/sirupsen/logrus" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" @@ -169,7 +168,7 @@ func (vs *f5VirtualServerSource) endpointsFromVirtualServers(virtualServers []*f targets = append(targets, virtualServer.Status.VSAddress) } - endpoints = append(endpoints, endpointsForHostname(virtualServer.Spec.Host, targets, ttl, nil, "", resource)...) + endpoints = append(endpoints, EndpointsForHostname(virtualServer.Spec.Host, targets, ttl, nil, "", resource)...) } return endpoints, nil @@ -192,12 +191,7 @@ func newVSUnstructuredConverter() (*unstructuredConverter, error) { // filterByAnnotations filters a list of VirtualServers by a given annotation selector. func (vs *f5VirtualServerSource) filterByAnnotations(virtualServers []*f5.VirtualServer) ([]*f5.VirtualServer, error) { - labelSelector, err := metav1.ParseToLabelSelector(vs.annotationFilter) - if err != nil { - return nil, err - } - - selector, err := metav1.LabelSelectorAsSelector(labelSelector) + selector, err := annotations.ParseFilter(vs.annotationFilter) if err != nil { return nil, err } diff --git a/source/gateway.go b/source/gateway.go index 811bf96de..445352838 100644 --- a/source/gateway.go +++ b/source/gateway.go @@ -242,7 +242,7 @@ func (src *gatewayRouteSource) Endpoints(ctx context.Context) ([]*endpoint.Endpo providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(annots) ttl := annotations.TTLFromAnnotations(annots, resource) for host, targets := range hostTargets { - routeEndpoints = append(routeEndpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier, resource)...) + routeEndpoints = append(routeEndpoints, EndpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier, resource)...) } log.Debugf("Endpoints generated from %s %s/%s: %v", src.rtKind, meta.Namespace, meta.Name, routeEndpoints) diff --git a/source/gloo_proxy.go b/source/gloo_proxy.go index 5c3e957a6..63eafecf0 100644 --- a/source/gloo_proxy.go +++ b/source/gloo_proxy.go @@ -169,7 +169,7 @@ func (gs *glooSource) generateEndpointsFromProxy(ctx context.Context, proxy *pro ttl := annotations.TTLFromAnnotations(ants, resource) providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(ants) for _, domain := range virtualHost.Domains { - endpoints = append(endpoints, endpointsForHostname(strings.TrimSuffix(domain, "."), targets, ttl, providerSpecific, setIdentifier, "")...) + endpoints = append(endpoints, EndpointsForHostname(strings.TrimSuffix(domain, "."), targets, ttl, providerSpecific, setIdentifier, "")...) } } } diff --git a/source/ingress.go b/source/ingress.go index d3b8f8d09..e0735eba0 100644 --- a/source/ingress.go +++ b/source/ingress.go @@ -204,7 +204,7 @@ func (sc *ingressSource) endpointsFromTemplate(ing *networkv1.Ingress) ([]*endpo var endpoints []*endpoint.Endpoint for _, hostname := range hostnames { - endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) + endpoints = append(endpoints, EndpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) } return endpoints, nil } @@ -299,7 +299,7 @@ func endpointsFromIngress(ing *networkv1.Ingress, ignoreHostnameAnnotation bool, if rule.Host == "" { continue } - definedHostsEndpoints = append(definedHostsEndpoints, endpointsForHostname(rule.Host, targets, ttl, providerSpecific, setIdentifier, resource)...) + definedHostsEndpoints = append(definedHostsEndpoints, EndpointsForHostname(rule.Host, targets, ttl, providerSpecific, setIdentifier, resource)...) } } @@ -310,7 +310,7 @@ func endpointsFromIngress(ing *networkv1.Ingress, ignoreHostnameAnnotation bool, if host == "" { continue } - definedHostsEndpoints = append(definedHostsEndpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier, resource)...) + definedHostsEndpoints = append(definedHostsEndpoints, EndpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier, resource)...) } } } @@ -319,7 +319,7 @@ func endpointsFromIngress(ing *networkv1.Ingress, ignoreHostnameAnnotation bool, var annotationEndpoints []*endpoint.Endpoint if !ignoreHostnameAnnotation { for _, hostname := range annotations.HostnamesFromAnnotations(ing.Annotations) { - annotationEndpoints = append(annotationEndpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) + annotationEndpoints = append(annotationEndpoints, EndpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) } } diff --git a/source/istio_gateway.go b/source/istio_gateway.go index 60b3d70a3..0d5c422d7 100644 --- a/source/istio_gateway.go +++ b/source/istio_gateway.go @@ -285,7 +285,7 @@ func (sc *gatewaySource) endpointsFromGateway(ctx context.Context, hostnames []s providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(gateway.Annotations) for _, host := range hostnames { - endpoints = append(endpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier, resource)...) + endpoints = append(endpoints, EndpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier, resource)...) } return endpoints, nil diff --git a/source/istio_virtualservice.go b/source/istio_virtualservice.go index 2f9aaf439..97b4e79ea 100644 --- a/source/istio_virtualservice.go +++ b/source/istio_virtualservice.go @@ -247,7 +247,7 @@ func (sc *virtualServiceSource) endpointsFromTemplate(ctx context.Context, virtu if err != nil { return endpoints, err } - endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) + endpoints = append(endpoints, EndpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) } return endpoints, nil } @@ -346,7 +346,7 @@ func (sc *virtualServiceSource) endpointsFromVirtualService(ctx context.Context, } } - endpoints = append(endpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier, resource)...) + endpoints = append(endpoints, EndpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier, resource)...) } // Skip endpoints if we do not want entries from annotations @@ -360,7 +360,7 @@ func (sc *virtualServiceSource) endpointsFromVirtualService(ctx context.Context, return endpoints, err } } - endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) + endpoints = append(endpoints, EndpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) } } diff --git a/source/kong_tcpingress.go b/source/kong_tcpingress.go index 6bdc5886b..76efc4372 100644 --- a/source/kong_tcpingress.go +++ b/source/kong_tcpingress.go @@ -199,14 +199,14 @@ func (sc *kongTCPIngressSource) endpointsFromTCPIngress(tcpIngress *TCPIngress, if !sc.ignoreHostnameAnnotation { hostnameList := annotations.HostnamesFromAnnotations(tcpIngress.Annotations) for _, hostname := range hostnameList { - endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) + endpoints = append(endpoints, EndpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) } } if tcpIngress.Spec.Rules != nil { for _, rule := range tcpIngress.Spec.Rules { if rule.Host != "" { - endpoints = append(endpoints, endpointsForHostname(rule.Host, targets, ttl, providerSpecific, setIdentifier, resource)...) + endpoints = append(endpoints, EndpointsForHostname(rule.Host, targets, ttl, providerSpecific, setIdentifier, resource)...) } } } diff --git a/source/openshift_route.go b/source/openshift_route.go index 40e12fec8..baa485cd8 100644 --- a/source/openshift_route.go +++ b/source/openshift_route.go @@ -190,7 +190,7 @@ func (ors *ocpRouteSource) endpointsFromTemplate(ocpRoute *routev1.Route) ([]*en var endpoints []*endpoint.Endpoint for _, hostname := range hostnames { - endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) + endpoints = append(endpoints, EndpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) } return endpoints, nil } @@ -236,14 +236,14 @@ func (ors *ocpRouteSource) endpointsFromOcpRoute(ocpRoute *routev1.Route, ignore providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(ocpRoute.Annotations) if host != "" { - endpoints = append(endpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier, resource)...) + endpoints = append(endpoints, EndpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier, resource)...) } // Skip endpoints if we do not want entries from annotations if !ignoreHostnameAnnotation { hostnameList := annotations.HostnamesFromAnnotations(ocpRoute.Annotations) for _, hostname := range hostnameList { - endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) + endpoints = append(endpoints, EndpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) } } return endpoints diff --git a/source/service.go b/source/service.go index 2f96b94d3..91cd113d8 100644 --- a/source/service.go +++ b/source/service.go @@ -29,7 +29,6 @@ import ( log "github.com/sirupsen/logrus" v1 "k8s.io/api/core/v1" discoveryv1 "k8s.io/api/discovery/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/types" kubeinformers "k8s.io/client-go/informers" @@ -293,11 +292,7 @@ func (sc *serviceSource) Endpoints(_ context.Context) ([]*endpoint.Endpoint, err func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname string, ttl endpoint.TTL) []*endpoint.Endpoint { var endpoints []*endpoint.Endpoint - labelSelector, err := metav1.ParseToLabelSelector(labels.Set(svc.Spec.Selector).AsSelectorPreValidated().String()) - if err != nil { - return nil - } - selector, err := metav1.LabelSelectorAsSelector(labelSelector) + selector, err := annotations.ParseFilter(labels.Set(svc.Spec.Selector).AsSelectorPreValidated().String()) if err != nil { return nil } @@ -571,7 +566,7 @@ func (sc *serviceSource) generateEndpoints(svc *v1.Service, hostname string, pro } } - endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) + endpoints = append(endpoints, EndpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) return endpoints } @@ -698,11 +693,7 @@ func (sc *serviceSource) nodesExternalTrafficPolicyTypeLocal(svc *v1.Service) [] // pods retrieve a slice of pods associated with the given Service func (sc *serviceSource) pods(svc *v1.Service) []*v1.Pod { - labelSelector, err := metav1.ParseToLabelSelector(labels.Set(svc.Spec.Selector).AsSelectorPreValidated().String()) - if err != nil { - return nil - } - selector, err := metav1.LabelSelectorAsSelector(labelSelector) + selector, err := annotations.ParseFilter(labels.Set(svc.Spec.Selector).AsSelectorPreValidated().String()) if err != nil { return nil } diff --git a/source/skipper_routegroup.go b/source/skipper_routegroup.go index bf5b37ef7..de7c9f620 100644 --- a/source/skipper_routegroup.go +++ b/source/skipper_routegroup.go @@ -320,7 +320,7 @@ func (sc *routeGroupSource) endpointsFromTemplate(rg *routeGroup) ([]*endpoint.E hostnameList := strings.Split(strings.ReplaceAll(hostnames, " ", ""), ",") for _, hostname := range hostnameList { hostname = strings.TrimSuffix(hostname, ".") - endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) + endpoints = append(endpoints, EndpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) } return endpoints, nil } @@ -351,14 +351,14 @@ func (sc *routeGroupSource) endpointsFromRouteGroup(rg *routeGroup) []*endpoint. if src == "" { continue } - endpoints = append(endpoints, endpointsForHostname(src, targets, ttl, providerSpecific, setIdentifier, resource)...) + endpoints = append(endpoints, EndpointsForHostname(src, targets, ttl, providerSpecific, setIdentifier, resource)...) } // Skip endpoints if we do not want entries from annotations if !sc.ignoreHostnameAnnotation { hostnameList := annotations.HostnamesFromAnnotations(rg.Metadata.Annotations) for _, hostname := range hostnameList { - endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) + endpoints = append(endpoints, EndpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) } } return endpoints diff --git a/source/traefik_proxy.go b/source/traefik_proxy.go index af2ccd1b9..710a8b626 100644 --- a/source/traefik_proxy.go +++ b/source/traefik_proxy.go @@ -388,7 +388,7 @@ func (ts *traefikSource) endpointsFromIngressRoute(ingressRoute *IngressRoute, t if !ts.ignoreHostnameAnnotation { hostnameList := annotations.HostnamesFromAnnotations(ingressRoute.Annotations) for _, hostname := range hostnameList { - endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) + endpoints = append(endpoints, EndpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) } } @@ -399,7 +399,7 @@ func (ts *traefikSource) endpointsFromIngressRoute(ingressRoute *IngressRoute, t // Checking for host = * is required, as Host(`*`) can be set if host != "*" && host != "" { - endpoints = append(endpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier, resource)...) + endpoints = append(endpoints, EndpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier, resource)...) } } } @@ -421,7 +421,7 @@ func (ts *traefikSource) endpointsFromIngressRouteTCP(ingressRoute *IngressRoute if !ts.ignoreHostnameAnnotation { hostnameList := annotations.HostnamesFromAnnotations(ingressRoute.Annotations) for _, hostname := range hostnameList { - endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) + endpoints = append(endpoints, EndpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) } } @@ -432,7 +432,7 @@ func (ts *traefikSource) endpointsFromIngressRouteTCP(ingressRoute *IngressRoute // Checking for host = * is required, as HostSNI(`*`) can be set // in the case of TLS passthrough if host != "*" && host != "" { - endpoints = append(endpoints, endpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier, resource)...) + endpoints = append(endpoints, EndpointsForHostname(host, targets, ttl, providerSpecific, setIdentifier, resource)...) } } } @@ -454,7 +454,7 @@ func (ts *traefikSource) endpointsFromIngressRouteUDP(ingressRoute *IngressRoute if !ts.ignoreHostnameAnnotation { hostnameList := annotations.HostnamesFromAnnotations(ingressRoute.Annotations) for _, hostname := range hostnameList { - endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) + endpoints = append(endpoints, EndpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier, resource)...) } } @@ -908,11 +908,7 @@ func extractEndpoints[T any]( // 4. Iterates through the resources and matches their annotations against the selector. // 5. Returns the filtered list of resources or an error if any step fails. func filterResourcesByAnnotations[T any](resources []*T, annotationFilter string, getAnnotations func(*T) map[string]string) ([]*T, error) { - labelSelector, err := metav1.ParseToLabelSelector(annotationFilter) - if err != nil { - return nil, err - } - selector, err := metav1.LabelSelectorAsSelector(labelSelector) + selector, err := annotations.ParseFilter(annotationFilter) if err != nil { return nil, err } From 69d3424d4dfb8f9ac282d292875100372fe918e3 Mon Sep 17 00:00:00 2001 From: vflaux <38909103+vflaux@users.noreply.github.com> Date: Sun, 6 Jul 2025 12:37:24 +0200 Subject: [PATCH 18/49] fix(cloudflare): fix action display in logs (#5550) --- provider/cloudflare/cloudflare.go | 2 +- provider/cloudflare/cloudflare_regional.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/provider/cloudflare/cloudflare.go b/provider/cloudflare/cloudflare.go index 574212da4..1c5f99aaa 100644 --- a/provider/cloudflare/cloudflare.go +++ b/provider/cloudflare/cloudflare.go @@ -579,7 +579,7 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud "record": change.ResourceRecord.Name, "type": change.ResourceRecord.Type, "ttl": change.ResourceRecord.TTL, - "action": change.Action, + "action": change.Action.String(), "zone": zoneID, } diff --git a/provider/cloudflare/cloudflare_regional.go b/provider/cloudflare/cloudflare_regional.go index 6e93e2c72..2e7fdbdb9 100644 --- a/provider/cloudflare/cloudflare_regional.go +++ b/provider/cloudflare/cloudflare_regional.go @@ -100,7 +100,7 @@ func (p *CloudFlareProvider) submitRegionalHostnameChange(ctx context.Context, r changeLog := log.WithFields(log.Fields{ "hostname": rhChange.Hostname, "region_key": rhChange.RegionKey, - "action": rhChange.action, + "action": rhChange.action.String(), "zone": resourceContainer.Identifier, }) if p.DryRun { From 5c42ed00c789952d632828af46bce280d33b3958 Mon Sep 17 00:00:00 2001 From: vflaux <38909103+vflaux@users.noreply.github.com> Date: Sun, 6 Jul 2025 20:19:25 +0200 Subject: [PATCH 19/49] refactor(provider/cloudflare): use local regionalHostname struct (#5615) --- provider/cloudflare/cloudflare.go | 2 +- provider/cloudflare/cloudflare_regional.go | 90 +++-- .../cloudflare/cloudflare_regional_test.go | 355 +++++++++--------- provider/cloudflare/cloudflare_test.go | 30 +- 4 files changed, 246 insertions(+), 231 deletions(-) diff --git a/provider/cloudflare/cloudflare.go b/provider/cloudflare/cloudflare.go index 1c5f99aaa..930370089 100644 --- a/provider/cloudflare/cloudflare.go +++ b/provider/cloudflare/cloudflare.go @@ -220,7 +220,7 @@ type CloudFlareProvider struct { type cloudFlareChange struct { Action changeAction ResourceRecord cloudflare.DNSRecord - RegionalHostname cloudflare.RegionalHostname + RegionalHostname regionalHostname CustomHostnames map[string]cloudflare.CustomHostname CustomHostnamesPrev []string } diff --git a/provider/cloudflare/cloudflare_regional.go b/provider/cloudflare/cloudflare_regional.go index 2e7fdbdb9..9b7b9f988 100644 --- a/provider/cloudflare/cloudflare_regional.go +++ b/provider/cloudflare/cloudflare_regional.go @@ -40,12 +40,17 @@ var recordTypeRegionalHostnameSupported = map[string]bool{ "CNAME": true, } -// RegionalHostnamesMap is a map of regional hostnames keyed by hostname. -type RegionalHostnamesMap map[string]cloudflare.RegionalHostname +type regionalHostname struct { + hostname string + regionKey string +} + +// regionalHostnamesMap is a map of regional hostnames keyed by hostname. +type regionalHostnamesMap map[string]regionalHostname type regionalHostnameChange struct { action changeAction - cloudflare.RegionalHostname + regionalHostname } func (z zoneService) ListDataLocalizationRegionalHostnames(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.ListDataLocalizationRegionalHostnamesParams) ([]cloudflare.RegionalHostname, error) { @@ -69,16 +74,16 @@ func (z zoneService) DeleteDataLocalizationRegionalHostname(ctx context.Context, // createDataLocalizationRegionalHostnameParams is a function that returns the appropriate RegionalHostname Param based on the cloudFlareChange passed in func createDataLocalizationRegionalHostnameParams(rhc regionalHostnameChange) cloudflare.CreateDataLocalizationRegionalHostnameParams { return cloudflare.CreateDataLocalizationRegionalHostnameParams{ - Hostname: rhc.Hostname, - RegionKey: rhc.RegionKey, + Hostname: rhc.hostname, + RegionKey: rhc.regionKey, } } // updateDataLocalizationRegionalHostnameParams is a function that returns the appropriate RegionalHostname Param based on the cloudFlareChange passed in func updateDataLocalizationRegionalHostnameParams(rhc regionalHostnameChange) cloudflare.UpdateDataLocalizationRegionalHostnameParams { return cloudflare.UpdateDataLocalizationRegionalHostnameParams{ - Hostname: rhc.Hostname, - RegionKey: rhc.RegionKey, + Hostname: rhc.hostname, + RegionKey: rhc.regionKey, } } @@ -98,8 +103,8 @@ func (p *CloudFlareProvider) submitRegionalHostnameChanges(ctx context.Context, // submitRegionalHostnameChange applies a single regional hostname change, returns false if it fails func (p *CloudFlareProvider) submitRegionalHostnameChange(ctx context.Context, rhChange regionalHostnameChange, resourceContainer *cloudflare.ResourceContainer) bool { changeLog := log.WithFields(log.Fields{ - "hostname": rhChange.Hostname, - "region_key": rhChange.RegionKey, + "hostname": rhChange.hostname, + "region_key": rhChange.regionKey, "action": rhChange.action.String(), "zone": resourceContainer.Identifier, }) @@ -124,7 +129,7 @@ func (p *CloudFlareProvider) submitRegionalHostnameChange(ctx context.Context, r } case cloudFlareDelete: changeLog.Debug("Deleting regional hostname") - if err := p.Client.DeleteDataLocalizationRegionalHostname(ctx, resourceContainer, rhChange.Hostname); err != nil { + if err := p.Client.DeleteDataLocalizationRegionalHostname(ctx, resourceContainer, rhChange.hostname); err != nil { changeLog.Errorf("failed to delete regional hostname: %v", err) return false } @@ -132,34 +137,37 @@ func (p *CloudFlareProvider) submitRegionalHostnameChange(ctx context.Context, r return true } -func (p *CloudFlareProvider) listDataLocalisationRegionalHostnames(ctx context.Context, resourceContainer *cloudflare.ResourceContainer) (RegionalHostnamesMap, error) { +func (p *CloudFlareProvider) listDataLocalisationRegionalHostnames(ctx context.Context, resourceContainer *cloudflare.ResourceContainer) (regionalHostnamesMap, error) { rhs, err := p.Client.ListDataLocalizationRegionalHostnames(ctx, resourceContainer, cloudflare.ListDataLocalizationRegionalHostnamesParams{}) if err != nil { return nil, convertCloudflareError(err) } - rhsMap := make(RegionalHostnamesMap) - for _, r := range rhs { - rhsMap[r.Hostname] = r + rhsMap := make(regionalHostnamesMap) + for _, rh := range rhs { + rhsMap[rh.Hostname] = regionalHostname{ + hostname: rh.Hostname, + regionKey: rh.RegionKey, + } } return rhsMap, nil } -// regionalHostname returns a RegionalHostname for the given endpoint. +// regionalHostname returns a regionalHostname for the given endpoint. // // If the regional services feature is not enabled or the record type does not support regional hostnames, -// it returns an empty RegionalHostname. +// it returns an empty regionalHostname. // If the endpoint has a specific region key set, it uses that; otherwise, it defaults to the region key configured in the provider. -func (p *CloudFlareProvider) regionalHostname(ep *endpoint.Endpoint) cloudflare.RegionalHostname { +func (p *CloudFlareProvider) regionalHostname(ep *endpoint.Endpoint) regionalHostname { if !p.RegionalServicesConfig.Enabled || !recordTypeRegionalHostnameSupported[ep.RecordType] { - return cloudflare.RegionalHostname{} + return regionalHostname{} } regionKey := p.RegionalServicesConfig.RegionKey if epRegionKey, exists := ep.GetProviderSpecificProperty(annotations.CloudflareRegionKey); exists { regionKey = epRegionKey } - return cloudflare.RegionalHostname{ - Hostname: ep.DNSName, - RegionKey: regionKey, + return regionalHostname{ + hostname: ep.DNSName, + regionKey: regionKey, } } @@ -192,7 +200,7 @@ func (p *CloudFlareProvider) addEnpointsProviderSpecificRegionKeyProperty(ctx co for _, ep := range supportedEndpoints { if rh, found := regionalHostnames[ep.DNSName]; found { - ep.SetProviderSpecificProperty(annotations.CloudflareRegionKey, rh.RegionKey) + ep.SetProviderSpecificProperty(annotations.CloudflareRegionKey, rh.regionKey) } } return nil @@ -203,67 +211,67 @@ func (p *CloudFlareProvider) addEnpointsProviderSpecificRegionKeyProperty(ctx co // If there is a delete and a create or update action for the same hostname, // The create or update takes precedence. // Returns an error for conflicting region keys. -func desiredRegionalHostnames(changes []*cloudFlareChange) ([]cloudflare.RegionalHostname, error) { - rhs := make(map[string]cloudflare.RegionalHostname) +func desiredRegionalHostnames(changes []*cloudFlareChange) ([]regionalHostname, error) { + rhs := make(map[string]regionalHostname) for _, change := range changes { - if change.RegionalHostname.Hostname == "" { + if change.RegionalHostname.hostname == "" { continue } - rh, found := rhs[change.RegionalHostname.Hostname] + rh, found := rhs[change.RegionalHostname.hostname] if !found { if change.Action == cloudFlareDelete { - rhs[change.RegionalHostname.Hostname] = cloudflare.RegionalHostname{ - Hostname: change.RegionalHostname.Hostname, - RegionKey: "", // Indicate that this regional hostname should not exists + rhs[change.RegionalHostname.hostname] = regionalHostname{ + hostname: change.RegionalHostname.hostname, + regionKey: "", // Indicate that this regional hostname should not exists } continue } - rhs[change.RegionalHostname.Hostname] = change.RegionalHostname + rhs[change.RegionalHostname.hostname] = change.RegionalHostname continue } if change.Action == cloudFlareDelete { // A previous regional hostname exists so we can skip this delete action continue } - if rh.RegionKey == "" { + if rh.regionKey == "" { // If the existing regional hostname has no region key, we can overwrite it - rhs[change.RegionalHostname.Hostname] = change.RegionalHostname + rhs[change.RegionalHostname.hostname] = change.RegionalHostname continue } - if rh.RegionKey != change.RegionalHostname.RegionKey { - return nil, fmt.Errorf("conflicting region keys for regional hostname %q: %q and %q", change.RegionalHostname.Hostname, rh.RegionKey, change.RegionalHostname.RegionKey) + if rh.regionKey != change.RegionalHostname.regionKey { + return nil, fmt.Errorf("conflicting region keys for regional hostname %q: %q and %q", change.RegionalHostname.hostname, rh.regionKey, change.RegionalHostname.regionKey) } } return slices.Collect(maps.Values(rhs)), nil } // regionalHostnamesChanges build a list of changes needed to synchronize the current regional hostnames state with the desired state. -func regionalHostnamesChanges(desired []cloudflare.RegionalHostname, regionalHostnames RegionalHostnamesMap) []regionalHostnameChange { +func regionalHostnamesChanges(desired []regionalHostname, regionalHostnames regionalHostnamesMap) []regionalHostnameChange { changes := make([]regionalHostnameChange, 0) for _, rh := range desired { - current, found := regionalHostnames[rh.Hostname] - if rh.RegionKey == "" { + current, found := regionalHostnames[rh.hostname] + if rh.regionKey == "" { // If the region key is empty, we don't want a regional hostname if !found { continue } changes = append(changes, regionalHostnameChange{ action: cloudFlareDelete, - RegionalHostname: rh, + regionalHostname: rh, }) continue } if !found { changes = append(changes, regionalHostnameChange{ action: cloudFlareCreate, - RegionalHostname: rh, + regionalHostname: rh, }) continue } - if rh.RegionKey != current.RegionKey { + if rh.regionKey != current.regionKey { changes = append(changes, regionalHostnameChange{ action: cloudFlareUpdate, - RegionalHostname: rh, + regionalHostname: rh, }) } } diff --git a/provider/cloudflare/cloudflare_regional_test.go b/provider/cloudflare/cloudflare_regional_test.go index fbbca0d90..1aafbebac 100644 --- a/provider/cloudflare/cloudflare_regional_test.go +++ b/provider/cloudflare/cloudflare_regional_test.go @@ -37,7 +37,14 @@ func (m *mockCloudFlareClient) ListDataLocalizationRegionalHostnames(ctx context if strings.Contains(rc.Identifier, "rherror") { return nil, fmt.Errorf("failed to list regional hostnames") } - return m.regionalHostnames[rc.Identifier], nil + rhs := make([]cloudflare.RegionalHostname, 0, len(m.regionalHostnames[rc.Identifier])) + for _, rh := range m.regionalHostnames[rc.Identifier] { + rhs = append(rhs, cloudflare.RegionalHostname{ + Hostname: rh.hostname, + RegionKey: rh.regionKey, + }) + } + return rhs, nil } func (m *mockCloudFlareClient) CreateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDataLocalizationRegionalHostnameParams) error { @@ -49,9 +56,9 @@ func (m *mockCloudFlareClient) CreateDataLocalizationRegionalHostname(ctx contex Name: "CreateDataLocalizationRegionalHostname", ZoneId: rc.Identifier, RecordId: "", - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: rp.Hostname, - RegionKey: rp.RegionKey, + RegionalHostname: regionalHostname{ + hostname: rp.Hostname, + regionKey: rp.RegionKey, }, }) return nil @@ -66,9 +73,9 @@ func (m *mockCloudFlareClient) UpdateDataLocalizationRegionalHostname(ctx contex Name: "UpdateDataLocalizationRegionalHostname", ZoneId: rc.Identifier, RecordId: "", - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: rp.Hostname, - RegionKey: rp.RegionKey, + RegionalHostname: regionalHostname{ + hostname: rp.Hostname, + regionKey: rp.RegionKey, }, }) return nil @@ -82,8 +89,8 @@ func (m *mockCloudFlareClient) DeleteDataLocalizationRegionalHostname(ctx contex Name: "DeleteDataLocalizationRegionalHostname", ZoneId: rc.Identifier, RecordId: "", - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: hostname, + RegionalHostname: regionalHostname{ + hostname: hostname, }, }) return nil @@ -93,14 +100,14 @@ func TestCloudflareRegionalHostnameActions(t *testing.T) { tests := []struct { name string records map[string]cloudflare.DNSRecord - regionalHostnames []cloudflare.RegionalHostname + regionalHostnames []regionalHostname endpoints []*endpoint.Endpoint want []MockAction }{ { name: "create", records: map[string]cloudflare.DNSRecord{}, - regionalHostnames: []cloudflare.RegionalHostname{}, + regionalHostnames: []regionalHostname{}, endpoints: []*endpoint.Endpoint{ { RecordType: "A", @@ -131,9 +138,9 @@ func TestCloudflareRegionalHostnameActions(t *testing.T) { { Name: "CreateDataLocalizationRegionalHostname", ZoneId: "001", - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "create.bar.com", - RegionKey: "eu", + RegionalHostname: regionalHostname{ + hostname: "create.bar.com", + regionKey: "eu", }, }, }, @@ -150,10 +157,10 @@ func TestCloudflareRegionalHostnameActions(t *testing.T) { Proxied: proxyDisabled, }, }, - regionalHostnames: []cloudflare.RegionalHostname{ + regionalHostnames: []regionalHostname{ { - Hostname: "update.bar.com", - RegionKey: "us", + hostname: "update.bar.com", + regionKey: "us", }, }, endpoints: []*endpoint.Endpoint{ @@ -186,9 +193,9 @@ func TestCloudflareRegionalHostnameActions(t *testing.T) { { Name: "UpdateDataLocalizationRegionalHostname", ZoneId: "001", - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "update.bar.com", - RegionKey: "eu", + RegionalHostname: regionalHostname{ + hostname: "update.bar.com", + regionKey: "eu", }, }, }, @@ -205,10 +212,10 @@ func TestCloudflareRegionalHostnameActions(t *testing.T) { Proxied: proxyDisabled, }, }, - regionalHostnames: []cloudflare.RegionalHostname{ + regionalHostnames: []regionalHostname{ { - Hostname: "delete.bar.com", - RegionKey: "us", + hostname: "delete.bar.com", + regionKey: "us", }, }, endpoints: []*endpoint.Endpoint{}, @@ -222,8 +229,8 @@ func TestCloudflareRegionalHostnameActions(t *testing.T) { { Name: "DeleteDataLocalizationRegionalHostname", ZoneId: "001", - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "delete.bar.com", + RegionalHostname: regionalHostname{ + hostname: "delete.bar.com", }, }, }, @@ -240,10 +247,10 @@ func TestCloudflareRegionalHostnameActions(t *testing.T) { Proxied: proxyDisabled, }, }, - regionalHostnames: []cloudflare.RegionalHostname{ + regionalHostnames: []regionalHostname{ { - Hostname: "nochange.bar.com", - RegionKey: "eu", + hostname: "nochange.bar.com", + regionKey: "eu", }, }, endpoints: []*endpoint.Endpoint{ @@ -273,7 +280,7 @@ func TestCloudflareRegionalHostnameActions(t *testing.T) { Records: map[string]map[string]cloudflare.DNSRecord{ "001": tt.records, }, - regionalHostnames: map[string][]cloudflare.RegionalHostname{ + regionalHostnames: map[string][]regionalHostname{ "001": tt.regionalHostnames, }, }, @@ -323,9 +330,9 @@ func TestCloudflareRegionalHostnameDefaults(t *testing.T) { { Name: "CreateDataLocalizationRegionalHostname", ZoneId: "001", - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "bar.com", - RegionKey: "us", + RegionalHostname: regionalHostname{ + hostname: "bar.com", + regionKey: "us", }, }, }, @@ -341,7 +348,7 @@ func Test_regionalHostname(t *testing.T) { tests := []struct { name string args args - want cloudflare.RegionalHostname + want regionalHostname }{ { name: "no region key", @@ -355,9 +362,9 @@ func Test_regionalHostname(t *testing.T) { RegionKey: "", }, }, - want: cloudflare.RegionalHostname{ - Hostname: "example.com", - RegionKey: "", + want: regionalHostname{ + hostname: "example.com", + regionKey: "", }, }, { @@ -372,9 +379,9 @@ func Test_regionalHostname(t *testing.T) { RegionKey: "us", }, }, - want: cloudflare.RegionalHostname{ - Hostname: "example.com", - RegionKey: "us", + want: regionalHostname{ + hostname: "example.com", + regionKey: "us", }, }, { @@ -395,9 +402,9 @@ func Test_regionalHostname(t *testing.T) { RegionKey: "us", }, }, - want: cloudflare.RegionalHostname{ - Hostname: "example.com", - RegionKey: "eu", + want: regionalHostname{ + hostname: "example.com", + regionKey: "eu", }, }, { @@ -418,9 +425,9 @@ func Test_regionalHostname(t *testing.T) { RegionKey: "us", }, }, - want: cloudflare.RegionalHostname{ - Hostname: "example.com", - RegionKey: "", + want: regionalHostname{ + hostname: "example.com", + regionKey: "", }, }, { @@ -441,7 +448,7 @@ func Test_regionalHostname(t *testing.T) { RegionKey: "us", }, }, - want: cloudflare.RegionalHostname{}, + want: regionalHostname{}, }, { name: "disabled", @@ -460,9 +467,9 @@ func Test_regionalHostname(t *testing.T) { Enabled: false, }, }, - want: cloudflare.RegionalHostname{ - Hostname: "", - RegionKey: "", + want: regionalHostname{ + hostname: "", + regionKey: "", }, }, } @@ -479,7 +486,7 @@ func Test_desiredDataLocalizationRegionalHostnames(t *testing.T) { tests := []struct { name string changes []*cloudFlareChange - want []cloudflare.RegionalHostname + want []regionalHostname wantErr bool }{ { @@ -501,23 +508,23 @@ func Test_desiredDataLocalizationRegionalHostnames(t *testing.T) { changes: []*cloudFlareChange{ { Action: cloudFlareCreate, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "example.com", - RegionKey: "eu", + RegionalHostname: regionalHostname{ + hostname: "example.com", + regionKey: "eu", }, }, { Action: cloudFlareUpdate, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "example.com", - RegionKey: "eu", + RegionalHostname: regionalHostname{ + hostname: "example.com", + regionKey: "eu", }, }, }, - want: []cloudflare.RegionalHostname{ + want: []regionalHostname{ { - Hostname: "example.com", - RegionKey: "eu", + hostname: "example.com", + regionKey: "eu", }, }, wantErr: false, @@ -527,16 +534,16 @@ func Test_desiredDataLocalizationRegionalHostnames(t *testing.T) { changes: []*cloudFlareChange{ { Action: cloudFlareCreate, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "example.com", - RegionKey: "eu", + RegionalHostname: regionalHostname{ + hostname: "example.com", + regionKey: "eu", }, }, { Action: cloudFlareUpdate, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "example.com", - RegionKey: "us", // Different region key + RegionalHostname: regionalHostname{ + hostname: "example.com", + regionKey: "us", // Different region key }, }, }, @@ -548,38 +555,38 @@ func Test_desiredDataLocalizationRegionalHostnames(t *testing.T) { changes: []*cloudFlareChange{ { Action: cloudFlareCreate, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "example1.com", - RegionKey: "eu", + RegionalHostname: regionalHostname{ + hostname: "example1.com", + regionKey: "eu", }, }, { Action: cloudFlareUpdate, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "example2.com", - RegionKey: "us", + RegionalHostname: regionalHostname{ + hostname: "example2.com", + regionKey: "us", }, }, { Action: cloudFlareDelete, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "example3.com", - RegionKey: "us", + RegionalHostname: regionalHostname{ + hostname: "example3.com", + regionKey: "us", }, }, }, - want: []cloudflare.RegionalHostname{ + want: []regionalHostname{ { - Hostname: "example1.com", - RegionKey: "eu", + hostname: "example1.com", + regionKey: "eu", }, { - Hostname: "example2.com", - RegionKey: "us", + hostname: "example2.com", + regionKey: "us", }, { - Hostname: "example3.com", - RegionKey: "", + hostname: "example3.com", + regionKey: "", }, }, wantErr: false, @@ -589,16 +596,16 @@ func Test_desiredDataLocalizationRegionalHostnames(t *testing.T) { changes: []*cloudFlareChange{ { Action: cloudFlareCreate, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "example.com", - RegionKey: "", // Empty region key + RegionalHostname: regionalHostname{ + hostname: "example.com", + regionKey: "", // Empty region key }, }, }, - want: []cloudflare.RegionalHostname{ + want: []regionalHostname{ { - Hostname: "example.com", - RegionKey: "", + hostname: "example.com", + regionKey: "", }, }, wantErr: false, @@ -608,23 +615,23 @@ func Test_desiredDataLocalizationRegionalHostnames(t *testing.T) { changes: []*cloudFlareChange{ { Action: cloudFlareCreate, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "example.com", - RegionKey: "", // Empty region key + RegionalHostname: regionalHostname{ + hostname: "example.com", + regionKey: "", // Empty region key }, }, { Action: cloudFlareUpdate, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "example.com", - RegionKey: "eu", + RegionalHostname: regionalHostname{ + hostname: "example.com", + regionKey: "eu", }, }, }, - want: []cloudflare.RegionalHostname{ + want: []regionalHostname{ { - Hostname: "example.com", - RegionKey: "eu", + hostname: "example.com", + regionKey: "eu", }, }, wantErr: false, @@ -634,23 +641,23 @@ func Test_desiredDataLocalizationRegionalHostnames(t *testing.T) { changes: []*cloudFlareChange{ { Action: cloudFlareCreate, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "example.com", - RegionKey: "eu", + RegionalHostname: regionalHostname{ + hostname: "example.com", + regionKey: "eu", }, }, { Action: cloudFlareUpdate, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "example.com", - RegionKey: "eu", // Empty region key + RegionalHostname: regionalHostname{ + hostname: "example.com", + regionKey: "eu", // Empty region key }, }, }, - want: []cloudflare.RegionalHostname{ + want: []regionalHostname{ { - Hostname: "example.com", - RegionKey: "eu", + hostname: "example.com", + regionKey: "eu", }, }, wantErr: false, @@ -660,23 +667,23 @@ func Test_desiredDataLocalizationRegionalHostnames(t *testing.T) { changes: []*cloudFlareChange{ { Action: cloudFlareDelete, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "example.com", - RegionKey: "eu", + RegionalHostname: regionalHostname{ + hostname: "example.com", + regionKey: "eu", }, }, { Action: cloudFlareCreate, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "example.com", - RegionKey: "eu", + RegionalHostname: regionalHostname{ + hostname: "example.com", + regionKey: "eu", }, }, }, - want: []cloudflare.RegionalHostname{ + want: []regionalHostname{ { - Hostname: "example.com", - RegionKey: "eu", + hostname: "example.com", + regionKey: "eu", }, }, wantErr: false, @@ -686,23 +693,23 @@ func Test_desiredDataLocalizationRegionalHostnames(t *testing.T) { changes: []*cloudFlareChange{ { Action: cloudFlareCreate, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "example.com", - RegionKey: "eu", + RegionalHostname: regionalHostname{ + hostname: "example.com", + regionKey: "eu", }, }, { Action: cloudFlareDelete, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "example.com", - RegionKey: "eu", + RegionalHostname: regionalHostname{ + hostname: "example.com", + regionKey: "eu", }, }, }, - want: []cloudflare.RegionalHostname{ + want: []regionalHostname{ { - Hostname: "example.com", - RegionKey: "eu", + hostname: "example.com", + regionKey: "eu", }, }, wantErr: false, @@ -718,10 +725,10 @@ func Test_desiredDataLocalizationRegionalHostnames(t *testing.T) { assert.NoError(t, err) } sort.Slice(got, func(i, j int) bool { - return got[i].Hostname < got[j].Hostname + return got[i].hostname < got[j].hostname }) sort.Slice(tt.want, func(i, j int) bool { - return tt.want[i].Hostname < tt.want[j].Hostname + return tt.want[i].hostname < tt.want[j].hostname }) assert.Equal(t, tt.want, got) }) @@ -731,74 +738,74 @@ func Test_desiredDataLocalizationRegionalHostnames(t *testing.T) { func Test_dataLocalizationRegionalHostnamesChanges(t *testing.T) { tests := []struct { name string - desired []cloudflare.RegionalHostname - regionalHostnames RegionalHostnamesMap + desired []regionalHostname + regionalHostnames regionalHostnamesMap want []regionalHostnameChange }{ { name: "empty desired and current lists", - desired: []cloudflare.RegionalHostname{}, - regionalHostnames: RegionalHostnamesMap{}, + desired: []regionalHostname{}, + regionalHostnames: regionalHostnamesMap{}, want: []regionalHostnameChange{}, }, { name: "multiple changes", - desired: []cloudflare.RegionalHostname{ + desired: []regionalHostname{ { - Hostname: "create.example.com", - RegionKey: "eu", + hostname: "create.example.com", + regionKey: "eu", }, { - Hostname: "update.example.com", - RegionKey: "eu", + hostname: "update.example.com", + regionKey: "eu", }, { - Hostname: "delete.example.com", - RegionKey: "", + hostname: "delete.example.com", + regionKey: "", }, { - Hostname: "nochange.example.com", - RegionKey: "us", + hostname: "nochange.example.com", + regionKey: "us", }, { - Hostname: "absent.example.com", - RegionKey: "", + hostname: "absent.example.com", + regionKey: "", }, }, - regionalHostnames: RegionalHostnamesMap{ - "update.example.com": cloudflare.RegionalHostname{ - Hostname: "update.example.com", - RegionKey: "us", + regionalHostnames: regionalHostnamesMap{ + "update.example.com": regionalHostname{ + hostname: "update.example.com", + regionKey: "us", }, - "delete.example.com": cloudflare.RegionalHostname{ - Hostname: "delete.example.com", - RegionKey: "ap", + "delete.example.com": regionalHostname{ + hostname: "delete.example.com", + regionKey: "ap", }, - "nochange.example.com": cloudflare.RegionalHostname{ - Hostname: "nochange.example.com", - RegionKey: "us", + "nochange.example.com": regionalHostname{ + hostname: "nochange.example.com", + regionKey: "us", }, }, want: []regionalHostnameChange{ { action: cloudFlareCreate, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "create.example.com", - RegionKey: "eu", + regionalHostname: regionalHostname{ + hostname: "create.example.com", + regionKey: "eu", }, }, { action: cloudFlareUpdate, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "update.example.com", - RegionKey: "eu", + regionalHostname: regionalHostname{ + hostname: "update.example.com", + regionKey: "eu", }, }, { action: cloudFlareDelete, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "delete.example.com", - RegionKey: "", + regionalHostname: regionalHostname{ + hostname: "delete.example.com", + regionKey: "", }, }, }, @@ -834,7 +841,7 @@ func TestApplyChangesWithRegionalHostnamesFaillures(t *testing.T) { t.Parallel() type fields struct { Records map[string]cloudflare.DNSRecord - RegionalHostnames []cloudflare.RegionalHostname + RegionalHostnames []regionalHostname RegionKey string } type args struct { @@ -869,7 +876,7 @@ func TestApplyChangesWithRegionalHostnamesFaillures(t *testing.T) { name: "create fails", fields: fields{ Records: map[string]cloudflare.DNSRecord{}, - RegionalHostnames: []cloudflare.RegionalHostname{}, + RegionalHostnames: []regionalHostname{}, RegionKey: "us", }, args: args{ @@ -899,8 +906,8 @@ func TestApplyChangesWithRegionalHostnamesFaillures(t *testing.T) { Content: "127.0.0.1", }, }, - RegionalHostnames: []cloudflare.RegionalHostname{ - {Hostname: "rherror.bar.com", RegionKey: "us"}, + RegionalHostnames: []regionalHostname{ + {hostname: "rherror.bar.com", regionKey: "us"}, }, RegionKey: "us", }, @@ -941,8 +948,8 @@ func TestApplyChangesWithRegionalHostnamesFaillures(t *testing.T) { Content: "127.0.0.1", }, }, - RegionalHostnames: []cloudflare.RegionalHostname{ - {Hostname: "rherror.bar.com", RegionKey: "us"}, + RegionalHostnames: []regionalHostname{ + {hostname: "rherror.bar.com", regionKey: "us"}, }, RegionKey: "us", }, @@ -964,7 +971,7 @@ func TestApplyChangesWithRegionalHostnamesFaillures(t *testing.T) { name: "conflicting regional keys", fields: fields{ Records: map[string]cloudflare.DNSRecord{}, - RegionalHostnames: []cloudflare.RegionalHostname{}, + RegionalHostnames: []regionalHostname{}, RegionKey: "us", }, args: args{ @@ -1008,7 +1015,7 @@ func TestApplyChangesWithRegionalHostnamesFaillures(t *testing.T) { Records: map[string]map[string]cloudflare.DNSRecord{ "001": records, }, - regionalHostnames: map[string][]cloudflare.RegionalHostname{ + regionalHostnames: map[string][]regionalHostname{ "001": tt.fields.RegionalHostnames, }, }, @@ -1034,7 +1041,7 @@ func TestApplyChangesWithRegionalHostnamesDryRun(t *testing.T) { t.Parallel() type fields struct { Records map[string]cloudflare.DNSRecord - RegionalHostnames []cloudflare.RegionalHostname + RegionalHostnames []regionalHostname RegionKey string } type args struct { @@ -1050,7 +1057,7 @@ func TestApplyChangesWithRegionalHostnamesDryRun(t *testing.T) { name: "create dry run", fields: fields{ Records: map[string]cloudflare.DNSRecord{}, - RegionalHostnames: []cloudflare.RegionalHostname{}, + RegionalHostnames: []regionalHostname{}, RegionKey: "us", }, args: args{ @@ -1080,8 +1087,8 @@ func TestApplyChangesWithRegionalHostnamesDryRun(t *testing.T) { Content: "127.0.0.1", }, }, - RegionalHostnames: []cloudflare.RegionalHostname{ - {Hostname: "foo.bar.com", RegionKey: "us"}, + RegionalHostnames: []regionalHostname{ + {hostname: "foo.bar.com", regionKey: "us"}, }, RegionKey: "us", }, @@ -1122,8 +1129,8 @@ func TestApplyChangesWithRegionalHostnamesDryRun(t *testing.T) { Content: "127.0.0.1", }, }, - RegionalHostnames: []cloudflare.RegionalHostname{ - {Hostname: "foo.bar.com", RegionKey: "us"}, + RegionalHostnames: []regionalHostname{ + {hostname: "foo.bar.com", regionKey: "us"}, }, RegionKey: "us", }, @@ -1157,7 +1164,7 @@ func TestApplyChangesWithRegionalHostnamesDryRun(t *testing.T) { Records: map[string]map[string]cloudflare.DNSRecord{ "001": records, }, - regionalHostnames: map[string][]cloudflare.RegionalHostname{ + regionalHostnames: map[string][]regionalHostname{ "001": tt.fields.RegionalHostnames, }, }, diff --git a/provider/cloudflare/cloudflare_test.go b/provider/cloudflare/cloudflare_test.go index b304d2116..79b670000 100644 --- a/provider/cloudflare/cloudflare_test.go +++ b/provider/cloudflare/cloudflare_test.go @@ -48,7 +48,7 @@ type MockAction struct { ZoneId string RecordId string RecordData cloudflare.DNSRecord - RegionalHostname cloudflare.RegionalHostname + RegionalHostname regionalHostname } type mockCloudFlareClient struct { @@ -60,7 +60,7 @@ type mockCloudFlareClient struct { listZonesContextError error dnsRecordsError error customHostnames map[string][]cloudflare.CustomHostname - regionalHostnames map[string][]cloudflare.RegionalHostname + regionalHostnames map[string][]regionalHostname } var ExampleDomain = []cloudflare.DNSRecord{ @@ -102,7 +102,7 @@ func NewMockCloudFlareClient() *mockCloudFlareClient { "002": {}, }, customHostnames: map[string][]cloudflare.CustomHostname{}, - regionalHostnames: map[string][]cloudflare.RegionalHostname{}, + regionalHostnames: map[string][]regionalHostname{}, } } @@ -1795,8 +1795,8 @@ func TestCloudFlareProvider_newCloudFlareChange(t *testing.T) { } change, _ := p.newCloudFlareChange(cloudFlareCreate, ep, ep.Targets[0], nil) - if change.RegionalHostname.RegionKey != "us" { - t.Errorf("expected region key to be 'us', but got '%s'", change.RegionalHostname.RegionKey) + if change.RegionalHostname.regionKey != "us" { + t.Errorf("expected region key to be 'us', but got '%s'", change.RegionalHostname.regionKey) } var freeValidCommentBuilder strings.Builder @@ -1949,8 +1949,8 @@ func TestCloudFlareProvider_submitChangesCNAME(t *testing.T) { ID: "1234567890", Content: "my-tunnel-guid-here.cfargotunnel.com", }, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "my-domain-here.app", + RegionalHostname: regionalHostname{ + hostname: "my-domain-here.app", }, }, { @@ -1961,9 +1961,9 @@ func TestCloudFlareProvider_submitChangesCNAME(t *testing.T) { ID: "9876543210", Content: "heritage=external-dns,external-dns/owner=default,external-dns/resource=service/external-dns/my-domain-here-app", }, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "my-domain-here.app", - RegionKey: "", + RegionalHostname: regionalHostname{ + hostname: "my-domain-here.app", + regionKey: "", }, }, } @@ -2012,8 +2012,8 @@ func TestCloudFlareProvider_submitChangesApex(t *testing.T) { ID: "1234567890", Content: "my-tunnel-guid-here.cfargotunnel.com", }, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "@", // APEX record + RegionalHostname: regionalHostname{ + hostname: "@", // APEX record }, }, { @@ -2024,9 +2024,9 @@ func TestCloudFlareProvider_submitChangesApex(t *testing.T) { ID: "9876543210", Content: "heritage=external-dns,external-dns/owner=default,external-dns/resource=service/external-dns/my-domain-here-app", }, - RegionalHostname: cloudflare.RegionalHostname{ - Hostname: "@", // APEX record - RegionKey: "", + RegionalHostname: regionalHostname{ + hostname: "@", // APEX record + regionKey: "", }, }, } From 252a5e016c5314a1ffc20e69e34603b0940c3e0c Mon Sep 17 00:00:00 2001 From: Ivan Ka <5395690+ivankatliarchuk@users.noreply.github.com> Date: Mon, 7 Jul 2025 18:19:28 +0100 Subject: [PATCH 20/49] feat(source/istio): support version 1.25+ (#5611) * feat(source/istio): support version 1.22 Signed-off-by: ivan katliarchuk * feat(source/istio): support version 1.22 Signed-off-by: ivan katliarchuk * feat(source/istio): support version 1.22 Signed-off-by: ivan katliarchuk * feat(source/istio): support version 1.22 Signed-off-by: ivan katliarchuk * feat(source/istio): support version 1.22 Signed-off-by: ivan katliarchuk * feat(source/istio): support version 1.22 Signed-off-by: ivan katliarchuk * feat(source/istio): support version 1.22 Signed-off-by: ivan katliarchuk * feat(source/istio): support version 1.25+ Co-authored-by: mthemis-provenir <168411899+mthemis-provenir@users.noreply.github.com> --------- Signed-off-by: ivan katliarchuk Co-authored-by: mthemis-provenir <168411899+mthemis-provenir@users.noreply.github.com> --- docs/sources/istio.md | 28 ++++--- registry/txt_test.go | 2 - source/istio_gateway.go | 58 ++++++------- source/istio_gateway_test.go | 6 +- source/istio_virtualservice.go | 121 ++++++++++++---------------- source/istio_virtualservice_test.go | 46 +++++------ 6 files changed, 126 insertions(+), 135 deletions(-) diff --git a/docs/sources/istio.md b/docs/sources/istio.md index 960deb823..e2c046e38 100644 --- a/docs/sources/istio.md +++ b/docs/sources/istio.md @@ -5,9 +5,13 @@ It is meant to supplement the other provider-specific setup tutorials. **Note:** Using the Istio Gateway source requires Istio >=1.0.0. -* Manifest (for clusters without RBAC enabled) -* Manifest (for clusters with RBAC enabled) -* Update existing ExternalDNS Deployment +**Note:** Currently supported versions are `1.25` and `1.26` with `v1beta1` stored version. + +- [Support status of Istio releases](https://istio.io/latest/docs/releases/supported-releases/) + +- Manifest (for clusters without RBAC enabled) +- Manifest (for clusters with RBAC enabled) +- Update existing ExternalDNS Deployment ## Manifest (for clusters without RBAC enabled) @@ -119,9 +123,9 @@ spec: ## Update existing ExternalDNS Deployment -* For clusters with running `external-dns`, you can just update the deployment. -* With access to the `kube-system` namespace, update the existing `external-dns` deployment. - * Add a parameter to the arguments of the container to create dns entries with `--source=istio-gateway`. +- For clusters with running `external-dns`, you can just update the deployment. +- With access to the `kube-system` namespace, update the existing `external-dns` deployment. + - Add a parameter to the arguments of the container to create dns entries with `--source=istio-gateway`. Execute the following command or update the argument. @@ -148,13 +152,13 @@ The following are relevant snippets from that tutorial. With automatic sidecar injection: ```bash -kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.6/samples/httpbin/httpbin.yaml +kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.25/samples/httpbin/httpbin.yaml ``` Otherwise: ```bash -kubectl apply -f <(istioctl kube-inject -f https://raw.githubusercontent.com/istio/istio/release-1.6/samples/httpbin/httpbin.yaml) +kubectl apply -f <(istioctl kube-inject -f https://raw.githubusercontent.com/istio/istio/release-1.25/samples/httpbin/httpbin.yaml) ``` ### Using a Gateway as a source @@ -320,13 +324,13 @@ EOF ## Debug ExternalDNS -* Look for the deployment pod to see the status +- Look for the deployment pod to see the status ```console$ kubectl get pods | grep external-dns external-dns-6b84999479-4knv9 1/1 Running 0 3h29m ``` -* Watch for the logs as follows +- Watch for the logs as follows ```console kubectl logs -f external-dns-6b84999479-4knv9 @@ -336,7 +340,7 @@ At this point, you can `create` or `update` any `Istio Gateway` object with `hos > **ATTENTION**: Make sure to specify those whose account is related to the DNS record. -* Successful executions will print the following +- Successful executions will print the following ```console time="2020-01-17T06:08:08Z" level=info msg="Desired change: CREATE httpbin.example.com A" @@ -345,7 +349,7 @@ time="2020-01-17T06:08:08Z" level=info msg="2 record(s) in zone example.com. wer time="2020-01-17T06:09:08Z" level=info msg="All records are already up to date, there are no changes for the matching hosted zones" ``` -* If there's any problem around `clusterrole`, you would see the errors showing wrong permissions: +- If there's any problem around `clusterrole`, you would see the errors showing wrong permissions: ```console source \"gateways\" in API group \"networking.istio.io\" at the cluster scope" diff --git a/registry/txt_test.go b/registry/txt_test.go index c71424058..d59ce90ef 100644 --- a/registry/txt_test.go +++ b/registry/txt_test.go @@ -18,7 +18,6 @@ package registry import ( "context" - "fmt" "reflect" "strings" "testing" @@ -1511,7 +1510,6 @@ func TestNewTXTScheme(t *testing.T) { assert.Nil(t, ctx.Value(provider.RecordsContextKey)) } err := r.ApplyChanges(ctx, changes) - fmt.Println(err) require.NoError(t, err) } diff --git a/source/istio_gateway.go b/source/istio_gateway.go index 0d5c422d7..9043cdcfe 100644 --- a/source/istio_gateway.go +++ b/source/istio_gateway.go @@ -24,10 +24,10 @@ import ( "text/template" log "github.com/sirupsen/logrus" - networkingv1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3" + networkingv1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" istioclient "istio.io/client-go/pkg/clientset/versioned" istioinformers "istio.io/client-go/pkg/informers/externalversions" - networkingv1alpha3informer "istio.io/client-go/pkg/informers/externalversions/networking/v1alpha3" + networkingv1beta1informer "istio.io/client-go/pkg/informers/externalversions/networking/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" kubeinformers "k8s.io/client-go/informers" @@ -57,7 +57,7 @@ type gatewaySource struct { combineFQDNAnnotation bool ignoreHostnameAnnotation bool serviceInformer coreinformers.ServiceInformer - gatewayInformer networkingv1alpha3informer.GatewayInformer + gatewayInformer networkingv1beta1informer.GatewayInformer } // NewIstioGatewaySource creates a new gatewaySource with the given config. @@ -81,10 +81,10 @@ func NewIstioGatewaySource( informerFactory := kubeinformers.NewSharedInformerFactoryWithOptions(kubeClient, 0, kubeinformers.WithNamespace(namespace)) serviceInformer := informerFactory.Core().V1().Services() istioInformerFactory := istioinformers.NewSharedInformerFactory(istioClient, 0) - gatewayInformer := istioInformerFactory.Networking().V1alpha3().Gateways() + gatewayInformer := istioInformerFactory.Networking().V1beta1().Gateways() // Add default resource event handlers to properly initialize informer. - serviceInformer.Informer().AddEventHandler( + _, _ = serviceInformer.Informer().AddEventHandler( cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { log.Debug("service added") @@ -92,7 +92,7 @@ func NewIstioGatewaySource( }, ) - gatewayInformer.Informer().AddEventHandler( + _, _ = gatewayInformer.Informer().AddEventHandler( cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { log.Debug("gateway added") @@ -127,7 +127,7 @@ func NewIstioGatewaySource( // Endpoints returns endpoint objects for each host-target combination that should be processed. // Retrieves all gateway resources in the source's namespace(s). func (sc *gatewaySource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) { - gwList, err := sc.istioClient.NetworkingV1alpha3().Gateways(sc.namespace).List(ctx, metav1.ListOptions{}) + gwList, err := sc.istioClient.NetworkingV1beta1().Gateways(sc.namespace).List(ctx, metav1.ListOptions{}) if err != nil { return nil, err } @@ -140,12 +140,14 @@ func (sc *gatewaySource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, e var endpoints []*endpoint.Endpoint + log.Debugf("Found %d gateways in namespace %s", len(gateways), sc.namespace) + for _, gateway := range gateways { // Check controller annotation to see if we are responsible. controller, ok := gateway.Annotations[controllerAnnotationKey] if ok && controller != controllerAnnotationValue { - log.Debugf("Skipping gateway %s/%s because controller value does not match, found: %s, required: %s", - gateway.Namespace, gateway.Name, controller, controllerAnnotationValue) + log.Debugf("Skipping gateway %s/%s,%s because controller value does not match, found: %s, required: %s", + gateway.Namespace, gateway.APIVersion, gateway.Name, controller, controllerAnnotationValue) continue } @@ -168,6 +170,8 @@ 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, ",")) + if len(gwHostnames) == 0 { log.Debugf("No hostnames could be generated from gateway %s/%s", gateway.Namespace, gateway.Name) continue @@ -183,10 +187,11 @@ func (sc *gatewaySource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, e continue } - log.Debugf("Endpoints generated from gateway: %s/%s: %v", gateway.Namespace, gateway.Name, gwEndpoints) + log.Debugf("Endpoints generated from %q '%s/%s.%s': %q", gateway.Kind, gateway.Namespace, gateway.APIVersion, gateway.Name, gwEndpoints) endpoints = append(endpoints, gwEndpoints...) } + // TODO: sort on endpoint creation for _, ep := range endpoints { sort.Sort(ep.Targets) } @@ -198,11 +203,11 @@ func (sc *gatewaySource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, e func (sc *gatewaySource) AddEventHandler(ctx context.Context, handler func()) { log.Debug("Adding event handler for Istio Gateway") - sc.gatewayInformer.Informer().AddEventHandler(eventHandlerFunc(handler)) + _, _ = sc.gatewayInformer.Informer().AddEventHandler(eventHandlerFunc(handler)) } // filterByAnnotations filters a list of configs by a given annotation selector. -func (sc *gatewaySource) filterByAnnotations(gateways []*networkingv1alpha3.Gateway) ([]*networkingv1alpha3.Gateway, error) { +func (sc *gatewaySource) filterByAnnotations(gateways []*networkingv1beta1.Gateway) ([]*networkingv1beta1.Gateway, error) { selector, err := annotations.ParseFilter(sc.annotationFilter) if err != nil { return nil, err @@ -213,7 +218,7 @@ func (sc *gatewaySource) filterByAnnotations(gateways []*networkingv1alpha3.Gate return gateways, nil } - var filteredList []*networkingv1alpha3.Gateway + var filteredList []*networkingv1beta1.Gateway for _, gw := range gateways { // include if the annotations match the selector @@ -225,7 +230,7 @@ func (sc *gatewaySource) filterByAnnotations(gateways []*networkingv1alpha3.Gate return filteredList, nil } -func (sc *gatewaySource) targetsFromIngress(ctx context.Context, ingressStr string, gateway *networkingv1alpha3.Gateway) (endpoint.Targets, error) { +func (sc *gatewaySource) targetsFromIngress(ctx context.Context, ingressStr string, gateway *networkingv1beta1.Gateway) (endpoint.Targets, error) { namespace, name, err := ParseIngress(ingressStr) if err != nil { return nil, fmt.Errorf("failed to parse Ingress annotation on Gateway (%s/%s): %w", gateway.Namespace, gateway.Name, err) @@ -251,7 +256,7 @@ func (sc *gatewaySource) targetsFromIngress(ctx context.Context, ingressStr stri return targets, nil } -func (sc *gatewaySource) targetsFromGateway(ctx context.Context, gateway *networkingv1alpha3.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 @@ -266,22 +271,21 @@ func (sc *gatewaySource) targetsFromGateway(ctx context.Context, gateway *networ } // endpointsFromGatewayConfig extracts the endpoints from an Istio Gateway Config object -func (sc *gatewaySource) endpointsFromGateway(ctx context.Context, hostnames []string, gateway *networkingv1alpha3.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 - resource := fmt.Sprintf("gateway/%s/%s", gateway.Namespace, gateway.Name) - - ttl := annotations.TTLFromAnnotations(gateway.Annotations, resource) - - targets := annotations.TargetsFromTargetAnnotation(gateway.Annotations) - if len(targets) == 0 { - targets, err = sc.targetsFromGateway(ctx, gateway) - if err != nil { - return nil, err - } + targets, err := sc.targetsFromGateway(ctx, gateway) + if err != nil { + return nil, err } + if len(targets) == 0 { + return endpoints, nil + } + + resource := fmt.Sprintf("gateway/%s/%s", gateway.Namespace, gateway.Name) + ttl := annotations.TTLFromAnnotations(gateway.Annotations, resource) providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(gateway.Annotations) for _, host := range hostnames { @@ -291,7 +295,7 @@ func (sc *gatewaySource) endpointsFromGateway(ctx context.Context, hostnames []s return endpoints, nil } -func (sc *gatewaySource) hostNamesFromGateway(gateway *networkingv1alpha3.Gateway) ([]string, error) { +func (sc *gatewaySource) hostNamesFromGateway(gateway *networkingv1beta1.Gateway) ([]string, error) { var hostnames []string for _, server := range gateway.Spec.Servers { for _, host := range server.Hosts { diff --git a/source/istio_gateway_test.go b/source/istio_gateway_test.go index 50f6e4059..21f57009e 100644 --- a/source/istio_gateway_test.go +++ b/source/istio_gateway_test.go @@ -24,8 +24,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" - networkingv1alpha3api "istio.io/api/networking/v1alpha3" - networkingv1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3" + networkingv1alpha3api "istio.io/api/networking/v1beta1" + networkingv1alpha3 "istio.io/client-go/pkg/apis/networking/v1beta1" istiofake "istio.io/client-go/pkg/clientset/versioned/fake" v1 "k8s.io/api/core/v1" networkv1 "k8s.io/api/networking/v1" @@ -1494,7 +1494,7 @@ func testGatewayEndpoints(t *testing.T) { fakeIstioClient := istiofake.NewSimpleClientset() for _, config := range ti.configItems { gatewayCfg := config.Config() - _, err := fakeIstioClient.NetworkingV1alpha3().Gateways(ti.targetNamespace).Create(context.Background(), gatewayCfg, metav1.CreateOptions{}) + _, err := fakeIstioClient.NetworkingV1beta1().Gateways(ti.targetNamespace).Create(context.Background(), gatewayCfg, metav1.CreateOptions{}) require.NoError(t, err) } diff --git a/source/istio_virtualservice.go b/source/istio_virtualservice.go index 97b4e79ea..d16c236b0 100644 --- a/source/istio_virtualservice.go +++ b/source/istio_virtualservice.go @@ -20,15 +20,16 @@ import ( "cmp" "context" "fmt" + "slices" "sort" "strings" "text/template" log "github.com/sirupsen/logrus" - networkingv1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3" + v1beta1 "istio.io/client-go/pkg/apis/networking/v1beta1" istioclient "istio.io/client-go/pkg/clientset/versioned" istioinformers "istio.io/client-go/pkg/informers/externalversions" - networkingv1alpha3informer "istio.io/client-go/pkg/informers/externalversions/networking/v1alpha3" + networkingv1beta1informer "istio.io/client-go/pkg/informers/externalversions/networking/v1beta1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" @@ -58,8 +59,8 @@ type virtualServiceSource struct { combineFQDNAnnotation bool ignoreHostnameAnnotation bool serviceInformer coreinformers.ServiceInformer - virtualserviceInformer networkingv1alpha3informer.VirtualServiceInformer - gatewayInformer networkingv1alpha3informer.GatewayInformer + vServiceInformer networkingv1beta1informer.VirtualServiceInformer + gatewayInformer networkingv1beta1informer.GatewayInformer } // NewIstioVirtualServiceSource creates a new virtualServiceSource with the given config. @@ -83,11 +84,11 @@ func NewIstioVirtualServiceSource( informerFactory := kubeinformers.NewSharedInformerFactoryWithOptions(kubeClient, 0, kubeinformers.WithNamespace(namespace)) serviceInformer := informerFactory.Core().V1().Services() istioInformerFactory := istioinformers.NewSharedInformerFactoryWithOptions(istioClient, 0, istioinformers.WithNamespace(namespace)) - virtualServiceInformer := istioInformerFactory.Networking().V1alpha3().VirtualServices() - gatewayInformer := istioInformerFactory.Networking().V1alpha3().Gateways() + virtualServiceInformer := istioInformerFactory.Networking().V1beta1().VirtualServices() + gatewayInformer := istioInformerFactory.Networking().V1beta1().Gateways() // Add default resource event handlers to properly initialize informer. - serviceInformer.Informer().AddEventHandler( + _, _ = serviceInformer.Informer().AddEventHandler( cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { log.Debug("service added") @@ -95,7 +96,7 @@ func NewIstioVirtualServiceSource( }, ) - virtualServiceInformer.Informer().AddEventHandler( + _, _ = virtualServiceInformer.Informer().AddEventHandler( cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { log.Debug("virtual service added") @@ -103,7 +104,7 @@ func NewIstioVirtualServiceSource( }, ) - gatewayInformer.Informer().AddEventHandler( + _, _ = gatewayInformer.Informer().AddEventHandler( cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { log.Debug("gateway added") @@ -131,7 +132,7 @@ func NewIstioVirtualServiceSource( combineFQDNAnnotation: combineFQDNAnnotation, ignoreHostnameAnnotation: ignoreHostnameAnnotation, serviceInformer: serviceInformer, - virtualserviceInformer: virtualServiceInformer, + vServiceInformer: virtualServiceInformer, gatewayInformer: gatewayInformer, }, nil } @@ -139,7 +140,7 @@ func NewIstioVirtualServiceSource( // Endpoints returns endpoint objects for each host-target combination that should be processed. // Retrieves all VirtualService resources in the source's namespace(s). func (sc *virtualServiceSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) { - virtualServices, err := sc.virtualserviceInformer.Lister().VirtualServices(sc.namespace).List(labels.Everything()) + virtualServices, err := sc.vServiceInformer.Lister().VirtualServices(sc.namespace).List(labels.Everything()) if err != nil { return nil, err } @@ -150,23 +151,25 @@ func (sc *virtualServiceSource) Endpoints(ctx context.Context) ([]*endpoint.Endp var endpoints []*endpoint.Endpoint - for _, virtualService := range virtualServices { + log.Debugf("Found %d virtualservice in namespace %s", len(virtualServices), sc.namespace) + + for _, vService := range virtualServices { // Check controller annotation to see if we are responsible. - controller, ok := virtualService.Annotations[controllerAnnotationKey] + controller, ok := vService.Annotations[controllerAnnotationKey] if ok && controller != controllerAnnotationValue { - log.Debugf("Skipping VirtualService %s/%s because controller value does not match, found: %s, required: %s", - virtualService.Namespace, virtualService.Name, controller, controllerAnnotationValue) + log.Debugf("Skipping VirtualService %s/%s.%s because controller value does not match, found: %s, required: %s", + vService.Namespace, vService.APIVersion, vService.Name, controller, controllerAnnotationValue) continue } - gwEndpoints, err := sc.endpointsFromVirtualService(ctx, virtualService) + gwEndpoints, err := sc.endpointsFromVirtualService(ctx, vService) if err != nil { return nil, err } // apply template if host is missing on VirtualService if (sc.combineFQDNAnnotation || len(gwEndpoints) == 0) && sc.fqdnTemplate != nil { - iEndpoints, err := sc.endpointsFromTemplate(ctx, virtualService) + iEndpoints, err := sc.endpointsFromTemplate(ctx, vService) if err != nil { return nil, err } @@ -179,14 +182,15 @@ func (sc *virtualServiceSource) Endpoints(ctx context.Context) ([]*endpoint.Endp } if len(gwEndpoints) == 0 { - log.Debugf("No endpoints could be generated from VirtualService %s/%s", virtualService.Namespace, virtualService.Name) + log.Debugf("No endpoints could be generated from VirtualService %s/%s", vService.Namespace, vService.Name) continue } - log.Debugf("Endpoints generated from VirtualService: %s/%s: %v", virtualService.Namespace, virtualService.Name, gwEndpoints) + log.Debugf("Endpoints generated from %q '%s/%s.%s': %q", vService.Kind, vService.Namespace, vService.APIVersion, vService.Name, gwEndpoints) endpoints = append(endpoints, gwEndpoints...) } + // TODO: sort on endpoint creation for _, ep := range endpoints { sort.Sort(ep.Targets) } @@ -198,16 +202,16 @@ func (sc *virtualServiceSource) Endpoints(ctx context.Context) ([]*endpoint.Endp func (sc *virtualServiceSource) AddEventHandler(_ context.Context, handler func()) { log.Debug("Adding event handler for Istio VirtualService") - sc.virtualserviceInformer.Informer().AddEventHandler(eventHandlerFunc(handler)) + _, _ = sc.vServiceInformer.Informer().AddEventHandler(eventHandlerFunc(handler)) } -func (sc *virtualServiceSource) getGateway(_ context.Context, gatewayStr string, virtualService *networkingv1alpha3.VirtualService) (*networkingv1alpha3.Gateway, error) { +func (sc *virtualServiceSource) getGateway(_ context.Context, gatewayStr string, virtualService *v1beta1.VirtualService) (*v1beta1.Gateway, error) { if gatewayStr == "" || gatewayStr == IstioMeshGateway { // This refers to "all sidecars in the mesh"; ignore. return nil, nil } - namespace, name, err := parseGateway(gatewayStr) + namespace, name, err := ParseIngress(gatewayStr) if err != nil { log.Debugf("Failed parsing gatewayStr %s of VirtualService %s/%s", gatewayStr, virtualService.Namespace, virtualService.Name) return nil, err @@ -229,7 +233,7 @@ func (sc *virtualServiceSource) getGateway(_ context.Context, gatewayStr string, return gateway, nil } -func (sc *virtualServiceSource) endpointsFromTemplate(ctx context.Context, virtualService *networkingv1alpha3.VirtualService) ([]*endpoint.Endpoint, error) { +func (sc *virtualServiceSource) endpointsFromTemplate(ctx context.Context, virtualService *v1beta1.VirtualService) ([]*endpoint.Endpoint, error) { hostnames, err := fqdn.ExecTemplate(sc.fqdnTemplate, virtualService) if err != nil { return nil, err @@ -253,7 +257,7 @@ func (sc *virtualServiceSource) endpointsFromTemplate(ctx context.Context, virtu } // filterByAnnotations filters a list of configs by a given annotation selector. -func (sc *virtualServiceSource) filterByAnnotations(virtualservices []*networkingv1alpha3.VirtualService) ([]*networkingv1alpha3.VirtualService, error) { +func (sc *virtualServiceSource) filterByAnnotations(vServices []*v1beta1.VirtualService) ([]*v1beta1.VirtualService, error) { selector, err := annotations.ParseFilter(sc.annotationFilter) if err != nil { return nil, err @@ -261,12 +265,12 @@ func (sc *virtualServiceSource) filterByAnnotations(virtualservices []*networkin // empty filter returns original list if selector.Empty() { - return virtualservices, nil + return vServices, nil } - var filteredList []*networkingv1alpha3.VirtualService + var filteredList []*v1beta1.VirtualService - for _, vs := range virtualservices { + for _, vs := range vServices { // include if the annotations match the selector if selector.Matches(labels.Set(vs.Annotations)) { filteredList = append(filteredList, vs) @@ -278,26 +282,24 @@ func (sc *virtualServiceSource) filterByAnnotations(virtualservices []*networkin // append a target to the list of targets unless it's already in the list func appendUnique(targets []string, target string) []string { - for _, element := range targets { - if element == target { - return targets - } + if slices.Contains(targets, target) { + return targets } return append(targets, target) } -func (sc *virtualServiceSource) targetsFromVirtualService(ctx context.Context, virtualService *networkingv1alpha3.VirtualService, vsHost string) ([]string, error) { +func (sc *virtualServiceSource) targetsFromVirtualService(ctx context.Context, vService *v1beta1.VirtualService, vsHost string) ([]string, error) { var targets []string // for each host we need to iterate through the gateways because each host might match for only one of the gateways - for _, gateway := range virtualService.Spec.Gateways { - gw, err := sc.getGateway(ctx, gateway, virtualService) + for _, gateway := range vService.Spec.Gateways { + gw, err := sc.getGateway(ctx, gateway, vService) if err != nil { return nil, err } if gw == nil { continue } - if !virtualServiceBindsToGateway(virtualService, gw, vsHost) { + if !virtualServiceBindsToGateway(vService, gw, vsHost) { continue } tgs, err := sc.targetsFromGateway(ctx, gw) @@ -308,24 +310,23 @@ func (sc *virtualServiceSource) targetsFromVirtualService(ctx context.Context, v targets = appendUnique(targets, target) } } - return targets, nil } // endpointsFromVirtualService extracts the endpoints from an Istio VirtualService Config object -func (sc *virtualServiceSource) endpointsFromVirtualService(ctx context.Context, virtualservice *networkingv1alpha3.VirtualService) ([]*endpoint.Endpoint, error) { +func (sc *virtualServiceSource) endpointsFromVirtualService(ctx context.Context, vService *v1beta1.VirtualService) ([]*endpoint.Endpoint, error) { var endpoints []*endpoint.Endpoint var err error - resource := fmt.Sprintf("virtualservice/%s/%s", virtualservice.Namespace, virtualservice.Name) + resource := fmt.Sprintf("virtualservice/%s/%s", vService.Namespace, vService.Name) - ttl := annotations.TTLFromAnnotations(virtualservice.Annotations, resource) + ttl := annotations.TTLFromAnnotations(vService.Annotations, resource) - targetsFromAnnotation := annotations.TargetsFromTargetAnnotation(virtualservice.Annotations) + targetsFromAnnotation := annotations.TargetsFromTargetAnnotation(vService.Annotations) - providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(virtualservice.Annotations) + providerSpecific, setIdentifier := annotations.ProviderSpecificAnnotations(vService.Annotations) - for _, host := range virtualservice.Spec.Hosts { + for _, host := range vService.Spec.Hosts { if host == "" || host == "*" { continue } @@ -340,7 +341,7 @@ func (sc *virtualServiceSource) endpointsFromVirtualService(ctx context.Context, targets := targetsFromAnnotation if len(targets) == 0 { - targets, err = sc.targetsFromVirtualService(ctx, virtualservice, host) + targets, err = sc.targetsFromVirtualService(ctx, vService, host) if err != nil { return endpoints, err } @@ -351,11 +352,11 @@ func (sc *virtualServiceSource) endpointsFromVirtualService(ctx context.Context, // Skip endpoints if we do not want entries from annotations if !sc.ignoreHostnameAnnotation { - hostnameList := annotations.HostnamesFromAnnotations(virtualservice.Annotations) + hostnameList := annotations.HostnamesFromAnnotations(vService.Annotations) for _, hostname := range hostnameList { targets := targetsFromAnnotation if len(targets) == 0 { - targets, err = sc.targetsFromVirtualService(ctx, virtualservice, hostname) + targets, err = sc.targetsFromVirtualService(ctx, vService, hostname) if err != nil { return endpoints, err } @@ -369,13 +370,13 @@ func (sc *virtualServiceSource) endpointsFromVirtualService(ctx context.Context, // checks if the given VirtualService should actually bind to the given gateway // see requirements here: https://istio.io/docs/reference/config/networking/gateway/#Server -func virtualServiceBindsToGateway(virtualService *networkingv1alpha3.VirtualService, gateway *networkingv1alpha3.Gateway, vsHost string) bool { +func virtualServiceBindsToGateway(vService *v1beta1.VirtualService, gateway *v1beta1.Gateway, vsHost string) bool { isValid := false - if len(virtualService.Spec.ExportTo) == 0 { + if len(vService.Spec.ExportTo) == 0 { isValid = true } else { - for _, ns := range virtualService.Spec.ExportTo { - if ns == "*" || ns == gateway.Namespace || (ns == "." && gateway.Namespace == virtualService.Namespace) { + for _, ns := range vService.Spec.ExportTo { + if ns == "*" || ns == gateway.Namespace || (ns == "." && gateway.Namespace == vService.Namespace) { isValid = true } } @@ -396,7 +397,7 @@ func virtualServiceBindsToGateway(virtualService *networkingv1alpha3.VirtualServ continue } - if namespace == "*" || namespace == virtualService.Namespace || (namespace == "." && virtualService.Namespace == gateway.Namespace) { + if namespace == "*" || namespace == vService.Namespace || (namespace == "." && vService.Namespace == gateway.Namespace) { if host == "*" { return true } @@ -416,23 +417,7 @@ func virtualServiceBindsToGateway(virtualService *networkingv1alpha3.VirtualServ return false } -// TODO: similar to ParseIngress -func parseGateway(gateway string) (string, string, error) { - var namespace, name string - var err error - parts := strings.Split(gateway, "/") - if len(parts) == 2 { - namespace, name = parts[0], parts[1] - } else if len(parts) == 1 { - name = parts[0] - } else { - err = fmt.Errorf("invalid gateway name (name or namespace/name) found '%v'", gateway) - } - - return namespace, name, err -} - -func (sc *virtualServiceSource) targetsFromIngress(ctx context.Context, ingressStr string, gateway *networkingv1alpha3.Gateway) (endpoint.Targets, error) { +func (sc *virtualServiceSource) targetsFromIngress(ctx context.Context, ingressStr string, gateway *v1beta1.Gateway) (endpoint.Targets, error) { namespace, name, err := ParseIngress(ingressStr) if err != nil { return nil, fmt.Errorf("failed to parse Ingress annotation on Gateway (%s/%s): %w", gateway.Namespace, gateway.Name, err) @@ -459,7 +444,7 @@ func (sc *virtualServiceSource) targetsFromIngress(ctx context.Context, ingressS return targets, nil } -func (sc *virtualServiceSource) targetsFromGateway(ctx context.Context, gateway *networkingv1alpha3.Gateway) (endpoint.Targets, error) { +func (sc *virtualServiceSource) targetsFromGateway(ctx context.Context, gateway *v1beta1.Gateway) (endpoint.Targets, error) { targets := annotations.TargetsFromTargetAnnotation(gateway.Annotations) if len(targets) > 0 { return targets, nil diff --git a/source/istio_virtualservice_test.go b/source/istio_virtualservice_test.go index 014b1d891..c4c01ba75 100644 --- a/source/istio_virtualservice_test.go +++ b/source/istio_virtualservice_test.go @@ -25,8 +25,8 @@ import ( "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "istio.io/api/meta/v1alpha1" - istionetworking "istio.io/api/networking/v1alpha3" - networkingv1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3" + 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" networkv1 "k8s.io/api/networking/v1" @@ -44,8 +44,8 @@ type VirtualServiceSuite struct { source Source lbServices []*v1.Service ingresses []*networkv1.Ingress - gwconfig *networkingv1alpha3.Gateway - vsconfig *networkingv1alpha3.VirtualService + gwconfig *networkingv1beta1.Gateway + vsconfig *networkingv1beta1.VirtualService } func (suite *VirtualServiceSuite) SetupTest() { @@ -98,7 +98,7 @@ func (suite *VirtualServiceSuite) SetupTest() { namespace: "istio-system", dnsnames: [][]string{{"*"}}, }).Config() - _, err = fakeIstioClient.NetworkingV1alpha3().Gateways(suite.gwconfig.Namespace).Create(context.Background(), suite.gwconfig, metav1.CreateOptions{}) + _, err = fakeIstioClient.NetworkingV1beta1().Gateways(suite.gwconfig.Namespace).Create(context.Background(), suite.gwconfig, metav1.CreateOptions{}) suite.NoError(err, "should succeed") suite.vsconfig = (fakeVirtualServiceConfig{ @@ -107,7 +107,7 @@ func (suite *VirtualServiceSuite) SetupTest() { gateways: []string{"istio-system/foo-gateway-with-targets"}, dnsnames: []string{"foo"}, }).Config() - _, err = fakeIstioClient.NetworkingV1alpha3().VirtualServices(suite.vsconfig.Namespace).Create(context.Background(), suite.vsconfig, metav1.CreateOptions{}) + _, err = fakeIstioClient.NetworkingV1beta1().VirtualServices(suite.vsconfig.Namespace).Create(context.Background(), suite.vsconfig, metav1.CreateOptions{}) suite.NoError(err, "should succeed") suite.source, err = NewIstioVirtualServiceSource( @@ -1948,8 +1948,8 @@ func testVirtualServiceEndpoints(t *testing.T) { t.Run(ti.title, func(t *testing.T) { t.Parallel() - var gateways []*networkingv1alpha3.Gateway - var virtualservices []*networkingv1alpha3.VirtualService + var gateways []*networkingv1beta1.Gateway + var virtualservices []*networkingv1beta1.VirtualService for _, gwItem := range ti.gwConfigs { gateways = append(gateways, gwItem.Config()) @@ -1958,7 +1958,7 @@ func testVirtualServiceEndpoints(t *testing.T) { virtualservices = append(virtualservices, vsItem.Config()) } - fakeKubernetesClient := fake.NewSimpleClientset() + fakeKubernetesClient := fake.NewClientset() for _, lb := range ti.lbServices { service := lb.Service() @@ -1975,12 +1975,12 @@ func testVirtualServiceEndpoints(t *testing.T) { fakeIstioClient := istiofake.NewSimpleClientset() for _, gateway := range gateways { - _, err := fakeIstioClient.NetworkingV1alpha3().Gateways(gateway.Namespace).Create(context.Background(), gateway, metav1.CreateOptions{}) + _, err := fakeIstioClient.NetworkingV1beta1().Gateways(gateway.Namespace).Create(context.Background(), gateway, metav1.CreateOptions{}) require.NoError(t, err) } - for _, virtualservice := range virtualservices { - _, err := fakeIstioClient.NetworkingV1alpha3().VirtualServices(virtualservice.Namespace).Create(context.Background(), virtualservice, metav1.CreateOptions{}) + for _, vService := range virtualservices { + _, err := fakeIstioClient.NetworkingV1beta1().VirtualServices(vService.Namespace).Create(context.Background(), vService, metav1.CreateOptions{}) require.NoError(t, err) } @@ -2041,7 +2041,7 @@ func testGatewaySelectorMatchesService(t *testing.T) { } func newTestVirtualServiceSource(loadBalancerList []fakeIngressGatewayService, ingressList []fakeIngress, gwList []fakeGatewayConfig) (*virtualServiceSource, error) { - fakeKubernetesClient := fake.NewSimpleClientset() + fakeKubernetesClient := fake.NewClientset() fakeIstioClient := istiofake.NewSimpleClientset() for _, lb := range loadBalancerList { @@ -2064,7 +2064,7 @@ func newTestVirtualServiceSource(loadBalancerList []fakeIngressGatewayService, i gwObj := gw.Config() // use create instead of add // https://github.com/kubernetes/client-go/blob/92512ee2b8cf6696e9909245624175b7f0c971d9/testing/fixture.go#LL336C3-L336C52 - _, err := fakeIstioClient.NetworkingV1alpha3().Gateways(gw.namespace).Create(context.Background(), gwObj, metav1.CreateOptions{}) + _, err := fakeIstioClient.NetworkingV1beta1().Gateways(gw.namespace).Create(context.Background(), gwObj, metav1.CreateOptions{}) if err != nil { return nil, err } @@ -2101,7 +2101,7 @@ type fakeVirtualServiceConfig struct { exportTo string } -func (c fakeVirtualServiceConfig) Config() *networkingv1alpha3.VirtualService { +func (c fakeVirtualServiceConfig) Config() *networkingv1beta1.VirtualService { vs := istionetworking.VirtualService{ Gateways: c.gateways, Hosts: c.dnsnames, @@ -2110,7 +2110,7 @@ func (c fakeVirtualServiceConfig) Config() *networkingv1alpha3.VirtualService { vs.ExportTo = []string{c.exportTo} } - return &networkingv1alpha3.VirtualService{ + return &networkingv1beta1.VirtualService{ ObjectMeta: metav1.ObjectMeta{ Name: c.name, Namespace: c.namespace, @@ -2127,13 +2127,13 @@ func TestVirtualServiceSourceGetGateway(t *testing.T) { type args struct { ctx context.Context gatewayStr string - virtualService *networkingv1alpha3.VirtualService + virtualService *networkingv1beta1.VirtualService } tests := []struct { name string fields fields args args - want *networkingv1alpha3.Gateway + want *networkingv1beta1.Gateway expectedErrStr string }{ {name: "EmptyGateway", fields: fields{ @@ -2155,7 +2155,7 @@ func TestVirtualServiceSourceGetGateway(t *testing.T) { }, args: args{ ctx: context.TODO(), gatewayStr: "doesnt/exist", - virtualService: &networkingv1alpha3.VirtualService{ + virtualService: &networkingv1beta1.VirtualService{ TypeMeta: metav1.TypeMeta{}, ObjectMeta: metav1.ObjectMeta{Name: "exist", Namespace: "doesnt"}, Spec: istionetworking.VirtualService{}, @@ -2167,8 +2167,8 @@ func TestVirtualServiceSourceGetGateway(t *testing.T) { }, args: args{ ctx: context.TODO(), gatewayStr: "1/2/3/", - virtualService: &networkingv1alpha3.VirtualService{}, - }, want: nil, expectedErrStr: "invalid gateway name (name or namespace/name) found '1/2/3/'"}, + virtualService: &networkingv1beta1.VirtualService{}, + }, want: nil, expectedErrStr: "invalid ingress name (name or namespace/name) found \"1/2/3/\""}, {name: "ExistingGateway", fields: fields{ virtualServiceSource: func() *virtualServiceSource { vs, _ := newTestVirtualServiceSource(nil, nil, []fakeGatewayConfig{{ @@ -2180,13 +2180,13 @@ func TestVirtualServiceSourceGetGateway(t *testing.T) { }, args: args{ ctx: context.TODO(), gatewayStr: "bar/foo", - virtualService: &networkingv1alpha3.VirtualService{ + virtualService: &networkingv1beta1.VirtualService{ TypeMeta: metav1.TypeMeta{}, ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "bar"}, Spec: istionetworking.VirtualService{}, Status: v1alpha1.IstioStatus{}, }, - }, want: &networkingv1alpha3.Gateway{ + }, want: &networkingv1beta1.Gateway{ TypeMeta: metav1.TypeMeta{}, ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "bar"}, Spec: istionetworking.Gateway{}, From 9045e45bc30ca32d72c4907b65158e114f65d62c Mon Sep 17 00:00:00 2001 From: Ivan Ka <5395690+ivankatliarchuk@users.noreply.github.com> Date: Tue, 8 Jul 2025 07:05:27 +0100 Subject: [PATCH 21/49] fix(source/service): make sure only unique targets pushed to registry (#5614) Signed-off-by: ivan katliarchuk --- endpoint/endpoint.go | 17 +- endpoint/endpoint_test.go | 43 +++++ source/service.go | 48 +++-- source/service_test.go | 376 +++++++++++++++++++++++++++++++++++++- 4 files changed, 457 insertions(+), 27 deletions(-) diff --git a/endpoint/endpoint.go b/endpoint/endpoint.go index 95461a5aa..68f82378a 100644 --- a/endpoint/endpoint.go +++ b/endpoint/endpoint.go @@ -19,6 +19,7 @@ package endpoint import ( "fmt" "net/netip" + "slices" "sort" "strconv" "strings" @@ -353,7 +354,21 @@ func (e *Endpoint) String() string { return fmt.Sprintf("%s %d IN %s %s %s %s", e.DNSName, e.RecordTTL, e.RecordType, e.SetIdentifier, e.Targets, e.ProviderSpecific) } -// Apply filter to slice of endpoints and return new filtered slice that includes +// UniqueOrderedTargets removes duplicate targets from the Endpoint and sorts them in lexicographical order. +func (e *Endpoint) UniqueOrderedTargets() { + result := make([]string, 0, len(e.Targets)) + existing := make(map[string]bool) + for _, target := range e.Targets { + if _, ok := existing[target]; !ok { + result = append(result, target) + existing[target] = true + } + } + slices.Sort(result) + e.Targets = result +} + +// FilterEndpointsByOwnerID Apply filter to slice of endpoints and return new filtered slice that includes // only endpoints that match. func FilterEndpointsByOwnerID(ownerID string, eps []*Endpoint) []*Endpoint { filtered := []*Endpoint{} diff --git a/endpoint/endpoint_test.go b/endpoint/endpoint_test.go index 0c2a3cbb9..d87aaab3c 100644 --- a/endpoint/endpoint_test.go +++ b/endpoint/endpoint_test.go @@ -925,3 +925,46 @@ func TestCheckEndpoint(t *testing.T) { }) } } + +func TestEndpoint_UniqueOrderedTargets(t *testing.T) { + tests := []struct { + name string + targets []string + expected Targets + want bool + }{ + { + name: "no duplicates", + targets: []string{"b.example.com", "a.example.com"}, + expected: Targets{"a.example.com", "b.example.com"}, + }, + { + name: "with duplicates", + targets: []string{"a.example.com", "b.example.com", "a.example.com"}, + expected: Targets{"a.example.com", "b.example.com"}, + }, + { + name: "already sorted", + targets: []string{"a.example.com", "b.example.com"}, + expected: Targets{"a.example.com", "b.example.com"}, + }, + { + name: "all duplicates", + targets: []string{"a.example.com", "a.example.com", "a.example.com"}, + expected: Targets{"a.example.com"}, + }, + { + name: "empty", + targets: []string{}, + expected: Targets{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ep := &Endpoint{Targets: tt.targets} + ep.UniqueOrderedTargets() + assert.Equal(t, tt.expected, ep.Targets) + }) + } +} diff --git a/source/service.go b/source/service.go index 91cd113d8..2606c0d8f 100644 --- a/source/service.go +++ b/source/service.go @@ -251,6 +251,29 @@ func (sc *serviceSource) Endpoints(_ context.Context) ([]*endpoint.Endpoint, err sort.Slice(endpoints, func(i, j int) bool { return endpoints[i].Labels[endpoint.ResourceLabelKey] < endpoints[j].Labels[endpoint.ResourceLabelKey] }) + mergedEndpoints := make(map[endpoint.EndpointKey][]*endpoint.Endpoint) + for _, ep := range endpoints { + key := ep.Key() + if existing, ok := mergedEndpoints[key]; ok { + if existing[0].RecordType == endpoint.RecordTypeCNAME { + log.Debugf("CNAME %s with multiple targets found", ep.DNSName) + mergedEndpoints[key] = append(existing, ep) + continue + } + existing[0].Targets = append(existing[0].Targets, ep.Targets...) + existing[0].UniqueOrderedTargets() + mergedEndpoints[key] = existing + } else { + ep.UniqueOrderedTargets() + mergedEndpoints[key] = []*endpoint.Endpoint{ep} + } + } + processed := make([]*endpoint.Endpoint, 0, len(mergedEndpoints)) + for _, ep := range mergedEndpoints { + processed = append(processed, ep...) + } + endpoints = processed + // Use stable sort to not disrupt the order of services sort.SliceStable(endpoints, func(i, j int) bool { if endpoints[i].DNSName != endpoints[j].DNSName { @@ -258,31 +281,6 @@ func (sc *serviceSource) Endpoints(_ context.Context) ([]*endpoint.Endpoint, err } return endpoints[i].RecordType < endpoints[j].RecordType }) - mergedEndpoints := []*endpoint.Endpoint{} - mergedEndpoints = append(mergedEndpoints, endpoints[0]) - for i := 1; i < len(endpoints); i++ { - lastMergedEndpoint := len(mergedEndpoints) - 1 - if mergedEndpoints[lastMergedEndpoint].DNSName == endpoints[i].DNSName && - mergedEndpoints[lastMergedEndpoint].RecordType == endpoints[i].RecordType && - mergedEndpoints[lastMergedEndpoint].RecordType != endpoint.RecordTypeCNAME && // It is against RFC-1034 for CNAME records to have multiple targets, so skip merging - mergedEndpoints[lastMergedEndpoint].SetIdentifier == endpoints[i].SetIdentifier && - mergedEndpoints[lastMergedEndpoint].RecordTTL == endpoints[i].RecordTTL { - mergedEndpoints[lastMergedEndpoint].Targets = append(mergedEndpoints[lastMergedEndpoint].Targets, endpoints[i].Targets[0]) - } else { - mergedEndpoints = append(mergedEndpoints, endpoints[i]) - } - - if mergedEndpoints[lastMergedEndpoint].DNSName == endpoints[i].DNSName && - mergedEndpoints[lastMergedEndpoint].RecordType == endpoints[i].RecordType && - mergedEndpoints[lastMergedEndpoint].RecordType == endpoint.RecordTypeCNAME { - log.Debugf("CNAME %s with multiple targets found", endpoints[i].DNSName) - } - } - endpoints = mergedEndpoints - } - - for _, ep := range endpoints { - sort.Sort(ep.Targets) } return endpoints, nil diff --git a/source/service_test.go b/source/service_test.go index 885af6959..406bdbc43 100644 --- a/source/service_test.go +++ b/source/service_test.go @@ -30,10 +30,12 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" discoveryv1 "k8s.io/api/discovery/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/client-go/kubernetes/fake" "sigs.k8s.io/external-dns/endpoint" @@ -3089,7 +3091,7 @@ func TestHeadlessServices(t *testing.T) { t.Parallel() // Create a Kubernetes testing client - kubernetes := fake.NewSimpleClientset() + kubernetes := fake.NewClientset() service := &v1.Service{ Spec: v1.ServiceSpec{ @@ -3196,6 +3198,378 @@ func TestHeadlessServices(t *testing.T) { } } +func TestMultipleHeadlessServicesPointingToPodsOnTheSameNode(t *testing.T) { + kubernetes := fake.NewClientset() + + headless := []*v1.Service{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "kafka", + Namespace: "default", + Labels: map[string]string{ + "app": "kafka", + }, + Annotations: map[string]string{ + annotations.HostnameKey: "example.org", + }, + }, + Spec: v1.ServiceSpec{ + Type: v1.ServiceTypeClusterIP, + ClusterIP: v1.ClusterIPNone, + ClusterIPs: []string{v1.ClusterIPNone}, + InternalTrafficPolicy: testutils.ToPtr(v1.ServiceInternalTrafficPolicyCluster), + IPFamilies: []v1.IPFamily{v1.IPv4Protocol}, + IPFamilyPolicy: testutils.ToPtr(v1.IPFamilyPolicySingleStack), + Ports: []v1.ServicePort{ + { + Name: "web", + Port: 80, + Protocol: v1.ProtocolTCP, + TargetPort: intstr.FromInt32(80), + }, + }, + Selector: map[string]string{ + "app": "kafka", + }, + SessionAffinity: v1.ServiceAffinityNone, + }, + Status: v1.ServiceStatus{ + LoadBalancer: v1.LoadBalancerStatus{}, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "kafka-2", + Namespace: "default", + Labels: map[string]string{ + "app": "kafka", + }, + Annotations: map[string]string{ + annotations.HostnameKey: "example.org", + }, + }, + Spec: v1.ServiceSpec{ + Type: v1.ServiceTypeClusterIP, + ClusterIP: v1.ClusterIPNone, + ClusterIPs: []string{v1.ClusterIPNone}, + InternalTrafficPolicy: testutils.ToPtr(v1.ServiceInternalTrafficPolicyCluster), + IPFamilies: []v1.IPFamily{v1.IPv4Protocol}, + IPFamilyPolicy: testutils.ToPtr(v1.IPFamilyPolicySingleStack), + Ports: []v1.ServicePort{ + { + Name: "web", + Port: 80, + Protocol: v1.ProtocolTCP, + TargetPort: intstr.FromInt32(80), + }, + }, + Selector: map[string]string{ + "app": "kafka", + }, + SessionAffinity: v1.ServiceAffinityNone, + }, + Status: v1.ServiceStatus{ + LoadBalancer: v1.LoadBalancerStatus{}, + }, + }, + } + + assert.NotNil(t, headless) + + pods := []*v1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "kafka-0", + Namespace: "default", + Labels: map[string]string{ + "app": "kafka", + appsv1.PodIndexLabel: "0", + appsv1.ControllerRevisionHashLabelKey: "kafka-b8d79cdb6", + appsv1.StatefulSetPodNameLabel: "kafka-0", + }, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "apps/v1", + Kind: "StatefulSet", + Name: "kafka", + }, + }, + }, + Spec: v1.PodSpec{ + Hostname: "kafka-0", + Subdomain: "kafka", + NodeName: "local-dev-worker", + Containers: []v1.Container{ + { + Name: "nginx", + Ports: []v1.ContainerPort{ + {Name: "web", ContainerPort: 80, Protocol: v1.ProtocolTCP}, + }, + }, + }, + }, + Status: v1.PodStatus{ + Phase: v1.PodRunning, + PodIP: "10.244.1.2", + PodIPs: []v1.PodIP{{IP: "10.244.1.2"}}, + HostIP: "172.18.0.2", + HostIPs: []v1.HostIP{{IP: "172.18.0.2"}}, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "kafka-1", + Namespace: "default", + Labels: map[string]string{ + "app": "kafka", + appsv1.PodIndexLabel: "1", + appsv1.ControllerRevisionHashLabelKey: "kafka-b8d79cdb6", + appsv1.StatefulSetPodNameLabel: "kafka-1", + }, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "apps/v1", + Kind: "StatefulSet", + Name: "kafka", + }, + }, + }, + Spec: v1.PodSpec{ + Hostname: "kafka-1", + Subdomain: "kafka", + NodeName: "local-dev-worker", + Containers: []v1.Container{ + { + Name: "nginx", + Ports: []v1.ContainerPort{ + {Name: "web", ContainerPort: 80, Protocol: v1.ProtocolTCP}, + }, + }, + }, + }, + Status: v1.PodStatus{ + Phase: v1.PodRunning, + PodIP: "10.244.1.3", + PodIPs: []v1.PodIP{{IP: "10.244.1.3"}}, + HostIP: "172.18.0.2", + HostIPs: []v1.HostIP{{IP: "172.18.0.2"}}, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "kafka-2", + Namespace: "default", + Labels: map[string]string{ + "app": "kafka", + appsv1.PodIndexLabel: "2", + appsv1.ControllerRevisionHashLabelKey: "kafka-b8d79cdb6", + appsv1.StatefulSetPodNameLabel: "kafka-2", + }, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "apps/v1", + Kind: "StatefulSet", + Name: "kafka", + }, + }, + }, + Spec: v1.PodSpec{ + Hostname: "kafka-2", + Subdomain: "kafka", + NodeName: "local-dev-worker", + Containers: []v1.Container{ + { + Name: "nginx", + Ports: []v1.ContainerPort{ + {Name: "web", ContainerPort: 80, Protocol: v1.ProtocolTCP}, + }, + }, + }, + }, + Status: v1.PodStatus{ + Phase: v1.PodRunning, + PodIP: "10.244.1.4", + PodIPs: []v1.PodIP{{IP: "10.244.1.4"}}, + HostIP: "172.18.0.2", + HostIPs: []v1.HostIP{{IP: "172.18.0.2"}}, + }, + }, + } + assert.Len(t, pods, 3) + + endpoints := []*discoveryv1.EndpointSlice{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "kafka-xhrc9", + Namespace: "default", + Labels: map[string]string{ + "app": "kafka", + discoveryv1.LabelServiceName: "kafka", + discoveryv1.LabelManagedBy: "endpointslice-controller.k8s.io", + v1.IsHeadlessService: "", + }, + }, + AddressType: discoveryv1.AddressTypeIPv4, + Endpoints: []discoveryv1.Endpoint{ + { + Addresses: []string{"10.244.1.2"}, + Hostname: testutils.ToPtr("kafka-0"), + NodeName: testutils.ToPtr("local-dev-worker"), + TargetRef: &v1.ObjectReference{ + Kind: "Pod", + Name: "kafka-0", + Namespace: "default", + }, + Conditions: discoveryv1.EndpointConditions{ + Ready: testutils.ToPtr(true), + Serving: testutils.ToPtr(true), + Terminating: testutils.ToPtr(false), + }, + }, + { + Addresses: []string{"10.244.1.3"}, + Hostname: testutils.ToPtr("kafka-1"), + NodeName: testutils.ToPtr("local-dev-worker"), + TargetRef: &v1.ObjectReference{ + Kind: "Pod", + Name: "kafka-1", + Namespace: "default", + }, + Conditions: discoveryv1.EndpointConditions{ + Ready: testutils.ToPtr(true), + Serving: testutils.ToPtr(true), + Terminating: testutils.ToPtr(false), + }, + }, + { + Addresses: []string{"10.244.1.4"}, + Hostname: testutils.ToPtr("kafka-2"), + NodeName: testutils.ToPtr("local-dev-worker"), + TargetRef: &v1.ObjectReference{ + Kind: "Pod", + Name: "kafka-2", + Namespace: "default", + }, + Conditions: discoveryv1.EndpointConditions{ + Ready: testutils.ToPtr(true), + Serving: testutils.ToPtr(true), + Terminating: testutils.ToPtr(false), + }, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "kafka-2-svwsg", + Namespace: "default", + Labels: map[string]string{ + "app": "kafka", + discoveryv1.LabelServiceName: "kafka-2", + discoveryv1.LabelManagedBy: "endpointslice-controller.k8s.io", + v1.IsHeadlessService: "", + }, + }, + AddressType: discoveryv1.AddressTypeIPv4, + Endpoints: []discoveryv1.Endpoint{ + { + Addresses: []string{"10.244.1.2"}, + Hostname: testutils.ToPtr("kafka-0"), + NodeName: testutils.ToPtr("local-dev-worker"), + TargetRef: &v1.ObjectReference{ + Kind: "Pod", + Name: "kafka-0", + Namespace: "default", + }, + Conditions: discoveryv1.EndpointConditions{ + Ready: testutils.ToPtr(true), + Serving: testutils.ToPtr(true), + Terminating: testutils.ToPtr(false), + }, + }, + { + Addresses: []string{"10.244.1.3"}, + Hostname: testutils.ToPtr("kafka-1"), + NodeName: testutils.ToPtr("local-dev-worker"), + TargetRef: &v1.ObjectReference{ + Kind: "Pod", + Name: "kafka-1", + Namespace: "default", + }, + Conditions: discoveryv1.EndpointConditions{ + Ready: testutils.ToPtr(true), + Serving: testutils.ToPtr(true), + Terminating: testutils.ToPtr(false), + }, + }, + { + Addresses: []string{"10.244.1.4"}, + Hostname: testutils.ToPtr("kafka-2"), + NodeName: testutils.ToPtr("local-dev-worker"), + TargetRef: &v1.ObjectReference{ + Kind: "Pod", + Name: "kafka-2", + Namespace: "default", + }, + Conditions: discoveryv1.EndpointConditions{ + Ready: testutils.ToPtr(true), + Serving: testutils.ToPtr(true), + Terminating: testutils.ToPtr(false), + }, + }, + }, + }, + } + + for _, svc := range headless { + _, err := kubernetes.CoreV1().Services(svc.Namespace).Create(context.Background(), svc, metav1.CreateOptions{}) + require.NoError(t, err) + } + + for _, pod := range pods { + _, err := kubernetes.CoreV1().Pods(pod.Namespace).Create(context.Background(), pod, metav1.CreateOptions{}) + require.NoError(t, err) + } + + for _, ep := range endpoints { + _, err := kubernetes.DiscoveryV1().EndpointSlices(ep.Namespace).Create(context.Background(), ep, metav1.CreateOptions{}) + require.NoError(t, err) + } + + src, err := NewServiceSource( + t.Context(), + kubernetes, + v1.NamespaceAll, + "", + "", + false, + "", + false, + false, + false, + []string{}, + false, + labels.Everything(), + false, + false, + false, + ) + require.NoError(t, err) + assert.NotNil(t, src) + + got, err := src.Endpoints(context.Background()) + require.NoError(t, err) + + want := []*endpoint.Endpoint{ + // TODO: root domain records should not be created. Address them in a follow-up PR. + {DNSName: "example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"10.244.1.2", "10.244.1.3", "10.244.1.4"}}, + {DNSName: "kafka-0.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"10.244.1.2"}}, + {DNSName: "kafka-1.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"10.244.1.3"}}, + {DNSName: "kafka-2.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"10.244.1.4"}}, + } + + validateEndpoints(t, got, want) +} + // TestHeadlessServices tests that headless services generate the correct endpoints. func TestHeadlessServicesHostIP(t *testing.T) { t.Parallel() From a32f241b7bc46ecaa0590e3b79cef30d97dc6e22 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Jul 2025 01:23:27 -0700 Subject: [PATCH 22/49] chore(deps): bump github.com/digitalocean/godo (#5623) Bumps the dev-dependencies group with 1 update: [github.com/digitalocean/godo](https://github.com/digitalocean/godo). Updates `github.com/digitalocean/godo` from 1.155.0 to 1.156.0 - [Release notes](https://github.com/digitalocean/godo/releases) - [Changelog](https://github.com/digitalocean/godo/blob/main/CHANGELOG.md) - [Commits](https://github.com/digitalocean/godo/compare/v1.155.0...v1.156.0) --- updated-dependencies: - dependency-name: github.com/digitalocean/godo dependency-version: 1.156.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dev-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 05e9dae5f..3678f5296 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,7 @@ require ( github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381 github.com/datawire/ambassador v1.12.4 github.com/denverdino/aliyungo v0.0.0-20230411124812-ab98a9173ace - github.com/digitalocean/godo v1.155.0 + github.com/digitalocean/godo v1.156.0 github.com/dnsimple/dnsimple-go v1.7.0 github.com/exoscale/egoscale v0.102.3 github.com/ffledgling/pdns-go v0.0.0-20180219074714-524e7daccd99 diff --git a/go.sum b/go.sum index 6a5c97361..13dc9bd5e 100644 --- a/go.sum +++ b/go.sum @@ -250,8 +250,8 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/digitalocean/godo v1.155.0 h1:+Y09Nz1TTXFSq5fdgSpqvCKfEpN35FU9WIOMuEuCwgg= -github.com/digitalocean/godo v1.155.0/go.mod h1:tYeiWY5ZXVpU48YaFv0M5irUFHXGorZpDNm7zzdWMzM= +github.com/digitalocean/godo v1.156.0 h1:nGod3u9cicC8wcc79e3HiTEix7KHqKjzgqE9cWQTPwk= +github.com/digitalocean/godo v1.156.0/go.mod h1:tYeiWY5ZXVpU48YaFv0M5irUFHXGorZpDNm7zzdWMzM= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/dnsimple/dnsimple-go v1.7.0 h1:JKu9xJtZ3SqOC+BuYgAWeab7+EEx0sz422vu8j611ZY= github.com/dnsimple/dnsimple-go v1.7.0/go.mod h1:EKpuihlWizqYafSnQHGCd/gyvy3HkEQJ7ODB4KdV8T8= From b27ec255aadfcf5b4b392dec8b2c7d66bccaba8d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Jul 2025 22:45:27 -0700 Subject: [PATCH 23/49] chore(deps): bump the dev-dependencies group with 3 updates (#5628) Bumps the dev-dependencies group with 3 updates: [github.com/digitalocean/godo](https://github.com/digitalocean/godo), [github.com/oracle/oci-go-sdk/v65](https://github.com/oracle/oci-go-sdk) and [github.com/scaleway/scaleway-sdk-go](https://github.com/scaleway/scaleway-sdk-go). Updates `github.com/digitalocean/godo` from 1.156.0 to 1.157.0 - [Release notes](https://github.com/digitalocean/godo/releases) - [Changelog](https://github.com/digitalocean/godo/blob/main/CHANGELOG.md) - [Commits](https://github.com/digitalocean/godo/compare/v1.156.0...v1.157.0) Updates `github.com/oracle/oci-go-sdk/v65` from 65.95.0 to 65.95.1 - [Release notes](https://github.com/oracle/oci-go-sdk/releases) - [Changelog](https://github.com/oracle/oci-go-sdk/blob/master/CHANGELOG.md) - [Commits](https://github.com/oracle/oci-go-sdk/compare/v65.95.0...v65.95.1) Updates `github.com/scaleway/scaleway-sdk-go` from 1.0.0-beta.33 to 1.0.0-beta.34 - [Release notes](https://github.com/scaleway/scaleway-sdk-go/releases) - [Changelog](https://github.com/scaleway/scaleway-sdk-go/blob/master/CHANGELOG.md) - [Commits](https://github.com/scaleway/scaleway-sdk-go/compare/v1.0.0-beta.33...v1.0.0-beta.34) --- updated-dependencies: - dependency-name: github.com/digitalocean/godo dependency-version: 1.157.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: github.com/oracle/oci-go-sdk/v65 dependency-version: 65.95.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: github.com/scaleway/scaleway-sdk-go dependency-version: 1.0.0-beta.34 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dev-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 3678f5296..77e12a811 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,7 @@ require ( github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381 github.com/datawire/ambassador v1.12.4 github.com/denverdino/aliyungo v0.0.0-20230411124812-ab98a9173ace - github.com/digitalocean/godo v1.156.0 + github.com/digitalocean/godo v1.157.0 github.com/dnsimple/dnsimple-go v1.7.0 github.com/exoscale/egoscale v0.102.3 github.com/ffledgling/pdns-go v0.0.0-20180219074714-524e7daccd99 @@ -43,14 +43,14 @@ require ( github.com/miekg/dns v1.1.66 github.com/openshift/api v0.0.0-20230607130528-611114dca681 github.com/openshift/client-go v0.0.0-20230607134213-3cd0021bbee3 - github.com/oracle/oci-go-sdk/v65 v65.95.0 + github.com/oracle/oci-go-sdk/v65 v65.95.1 github.com/ovh/go-ovh v1.9.0 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pluralsh/gqlclient v1.12.2 github.com/projectcontour/contour v1.32.0 github.com/prometheus/client_golang v1.22.0 github.com/prometheus/client_model v0.6.2 - github.com/scaleway/scaleway-sdk-go v1.0.0-beta.33 + github.com/scaleway/scaleway-sdk-go v1.0.0-beta.34 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.10.0 github.com/transip/gotransip/v6 v6.26.0 diff --git a/go.sum b/go.sum index 13dc9bd5e..9d8b33e67 100644 --- a/go.sum +++ b/go.sum @@ -250,8 +250,8 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/digitalocean/godo v1.156.0 h1:nGod3u9cicC8wcc79e3HiTEix7KHqKjzgqE9cWQTPwk= -github.com/digitalocean/godo v1.156.0/go.mod h1:tYeiWY5ZXVpU48YaFv0M5irUFHXGorZpDNm7zzdWMzM= +github.com/digitalocean/godo v1.157.0 h1:ReELaS6FxXNf8gryUiVH0wmyUmZN8/NCmBX4gXd3F0o= +github.com/digitalocean/godo v1.157.0/go.mod h1:tYeiWY5ZXVpU48YaFv0M5irUFHXGorZpDNm7zzdWMzM= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/dnsimple/dnsimple-go v1.7.0 h1:JKu9xJtZ3SqOC+BuYgAWeab7+EEx0sz422vu8j611ZY= github.com/dnsimple/dnsimple-go v1.7.0/go.mod h1:EKpuihlWizqYafSnQHGCd/gyvy3HkEQJ7ODB4KdV8T8= @@ -819,8 +819,8 @@ github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxS github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= -github.com/oracle/oci-go-sdk/v65 v65.95.0 h1:fI+/mfJOS2DkQ+/AFSyJAfn1XFR4TTGm2AhN6xbsi00= -github.com/oracle/oci-go-sdk/v65 v65.95.0/go.mod h1:u6XRPsw9tPziBh76K7GrrRXPa8P8W3BQeqJ6ZZt9VLA= +github.com/oracle/oci-go-sdk/v65 v65.95.1 h1:KCYeX+c+A0ezLVgXKpEcgXNukn6wBkD9oWoSV2FlaUs= +github.com/oracle/oci-go-sdk/v65 v65.95.1/go.mod h1:u6XRPsw9tPziBh76K7GrrRXPa8P8W3BQeqJ6ZZt9VLA= github.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE= github.com/ovh/go-ovh v1.9.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c= github.com/oxtoacart/bpool v0.0.0-20150712133111-4e1c5567d7c2 h1:CXwSGu/LYmbjEab5aMCs5usQRVBGThelUKBNnoSOuso= @@ -916,8 +916,8 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/scaleway/scaleway-sdk-go v1.0.0-beta.33 h1:KhF0WejiUTDbL5X55nXowP7zNopwpowa6qaMAWyIE+0= -github.com/scaleway/scaleway-sdk-go v1.0.0-beta.33/go.mod h1:792k1RTU+5JeMXm35/e2Wgp71qPH/DmDoZrRc+EFZDk= +github.com/scaleway/scaleway-sdk-go v1.0.0-beta.34 h1:48+VFHsyVcAHIN2v1Ao9v1/RkjJS5AwctFucBrfYNIA= +github.com/scaleway/scaleway-sdk-go v1.0.0-beta.34/go.mod h1:zFWiHphneiey3s8HOtAEnGrRlWivNaxW5T6d5Xfco7g= github.com/schollz/progressbar/v3 v3.8.6 h1:QruMUdzZ1TbEP++S1m73OqRJk20ON11m6Wqv4EoGg8c= github.com/schollz/progressbar/v3 v3.8.6/go.mod h1:W5IEwbJecncFGBvuEh4A7HT1nZZ6WNIL2i3qbnI0WKY= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= From 1eccb64bcbbc6c42bec9a0014338e7d4ecb9f693 Mon Sep 17 00:00:00 2001 From: Ivan Ka <5395690+ivankatliarchuk@users.noreply.github.com> Date: Wed, 9 Jul 2025 08:43:28 +0100 Subject: [PATCH 24/49] fix(source/service): headless records and root/base domain (#5624) Signed-off-by: ivan katliarchuk --- docs/annotations/annotations.md | 67 +++++++++++++++++++++++++++++++++ docs/tutorials/hostport.md | 24 ++++++------ 2 files changed, 80 insertions(+), 11 deletions(-) diff --git a/docs/annotations/annotations.md b/docs/annotations/annotations.md index c5c172f23..bc3cffc10 100644 --- a/docs/annotations/annotations.md +++ b/docs/annotations/annotations.md @@ -66,6 +66,73 @@ Multiple hostnames can be specified through a comma-separated list, e.g. For `Pods`, uses the `Pod`'s `Status.PodIP`, unless they are `hostNetwork: true` in which case the NodeExternalIP is used for IPv4 and NodeInternalIP for IPv6. +Notes: + +- This annotation `overrides` any automatically derived hostnames (e.g., from Ingress.spec.rules[].host). +- Hostnames must match the domain filter set in ExternalDNS (e.g., --domain-filter=example.com). +- This is an alpha annotation — subject to change; newer versions may support alternatives or deprecate it. +- This annotation is helpful for: + - Services or other resources without native hostname fields. + - Explicit overrides or multi-host situations. + - Avoiding reliance on auto-detection or heuristics. + +### Use Cases for `external-dns.alpha.kubernetes.io/hostname` annotation + +#### Explicit Hostname Mapping for Services + +You have a Service (e.g. of type LoadBalancer or ClusterIP) and want to expose it under a custom DNS name: + +```yml +apiVersion: v1 +kind: Service +metadata: + name: my-service + annotations: + external-dns.alpha.kubernetes.io/hostname: app.example.com +spec: + type: LoadBalancer + ... +``` + +> ExternalDNS will create a A or CNAME record for app.example.com pointing to the external IP or hostname of the service. + +#### Multi-Hostname Records + +You can assign multiple hostnames by separating them with commas: + +```yml +annotations: + external-dns.alpha.kubernetes.io/hostname: api.example.com,api.internal.example.com +``` + +> ExternalDNS will create two DNS records for the same service. + +#### Static DNS Assignment Without Ingress Rules + +When using Ingress, you usually declare hostnames in the spec.rules[].host. But with this annotation, you can manage DNS independently: + +```yml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: my-ingress + annotations: + external-dns.alpha.kubernetes.io/hostname: www.example.com +spec: + rules: + - http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: my-service + port: + number: 80 +``` + +> Useful when DNS management is decoupled from routing logic. + ## external-dns.alpha.kubernetes.io/ingress-hostname-source Specifies where to get the domain for an `Ingress` resource. diff --git a/docs/tutorials/hostport.md b/docs/tutorials/hostport.md index 30a3f31c1..a789459ea 100644 --- a/docs/tutorials/hostport.md +++ b/docs/tutorials/hostport.md @@ -169,7 +169,7 @@ Very important here, is to set the `hostPort`(only works if the PodSecurityPolic Now we need to define a headless service to use to expose the Kafka pods. There are generally two approaches to use expose the nodeport of a Headless service: -1. Add `--fqdn-template={{name}}.example.org` +1. Add `--fqdn-template={{ .Name }}.example.org` 2. Use a full annotation If you go with #1, you just need to define the headless service, here is an example of the case #2: @@ -190,22 +190,24 @@ spec: component: kafka ``` -This will create 3 dns records: +This will create 4 dns records: ```sh -kafka-0.example.org -kafka-1.example.org -kafka-2.example.org +kafka-0.example.org IP-0 +kafka-1.example.org IP-1 +kafka-2.example.org IP-2 +example.org IP-0,IP-1,IP-2 ``` -If you set `--fqdn-template={{name}}.example.org` you can omit the annotation. -Generally it is a better approach to use `--fqdn-template={{name}}.example.org`, because then -you would get the service name inside the generated A records: +> !Notice rood domain with records `example.org` + +If you set `--fqdn-template={{ .Name }}.example.org` you can omit the annotation. ```sh -kafka-0.ksvc.example.org -kafka-1.ksvc.example.org -kafka-2.ksvc.example.org +kafka-0.ksvc.example.org IP-0 +kafka-1.ksvc.example.org IP-1 +kafka-2.ksvc.example.org IP-2 +ksvc.example.org IP-0,IP-1,IP-2 ``` #### Using pods' HostIPs as targets From 5d8d424bcbbbc0cd347153321d6afc92d4d3ebbc Mon Sep 17 00:00:00 2001 From: Ivan Ka <5395690+ivankatliarchuk@users.noreply.github.com> Date: Wed, 9 Jul 2025 08:43:35 +0100 Subject: [PATCH 25/49] chore(codebase): remove pointer to an interface (#5625) * chore(codebase): remove pointer to an interface Signed-off-by: ivan katliarchuk * chore(codebase): simplify logic Signed-off-by: ivan katliarchuk --------- Signed-off-by: ivan katliarchuk --- source/crd.go | 18 +++++++----------- source/crd_test.go | 2 +- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/source/crd.go b/source/crd.go index 011915a37..2911f1f6d 100644 --- a/source/crd.go +++ b/source/crd.go @@ -51,7 +51,7 @@ type crdSource struct { codec runtime.ParameterCodec annotationFilter string labelSelector labels.Selector - informer *cache.SharedInformer + informer cache.SharedInformer } func addKnownTypes(scheme *runtime.Scheme, groupVersion schema.GroupVersion) error { @@ -123,7 +123,7 @@ func NewCRDSource(crdClient rest.Interface, namespace, kind string, annotationFi if startInformer { // external-dns already runs its sync-handler periodically (controlled by `--interval` flag) to ensure any // missed or dropped events are handled. specify resync period 0 to avoid unnecessary sync handler invocations. - informer := cache.NewSharedInformer( + sourceCrd.informer = cache.NewSharedInformer( &cache.ListWatch{ ListWithContextFunc: func(ctx context.Context, lo metav1.ListOptions) (runtime.Object, error) { return sourceCrd.List(ctx, &lo) @@ -134,8 +134,7 @@ func NewCRDSource(crdClient rest.Interface, namespace, kind string, annotationFi }, &apiv1alpha1.DNSEndpoint{}, 0) - sourceCrd.informer = &informer - go informer.Run(wait.NeverStop) + go sourceCrd.informer.Run(wait.NeverStop) } return &sourceCrd, nil } @@ -145,8 +144,7 @@ func (cs *crdSource) AddEventHandler(_ context.Context, handler func()) { log.Debug("Adding event handler for CRD") // Right now there is no way to remove event handler from informer, see: // https://github.com/kubernetes/kubernetes/issues/79610 - informer := *cs.informer - _, _ = informer.AddEventHandler( + _, _ = cs.informer.AddEventHandler( cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { handler() @@ -190,11 +188,9 @@ func (cs *crdSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error illegalTarget := false for _, target := range ep.Targets { - if ep.RecordType != endpoint.RecordTypeNAPTR && strings.HasSuffix(target, ".") { - illegalTarget = true - break - } - if ep.RecordType == endpoint.RecordTypeNAPTR && !strings.HasSuffix(target, ".") { + isNAPTR := ep.RecordType == endpoint.RecordTypeNAPTR + hasDot := strings.HasSuffix(target, ".") + if (isNAPTR && !hasDot) || (!isNAPTR && hasDot) { illegalTarget = true break } diff --git a/source/crd_test.go b/source/crd_test.go index 6214c74b3..0f92d0db3 100644 --- a/source/crd_test.go +++ b/source/crd_test.go @@ -736,7 +736,7 @@ func helperCreateWatcherWithInformer(t *testing.T) (*cachetesting.FakeController }, time.Second, 10*time.Millisecond) cs := &crdSource{ - informer: &informer, + informer: informer, } return watcher, *cs From ad653a63b30bd69cf2f4085190fbdfbfa182ce83 Mon Sep 17 00:00:00 2001 From: Valerian Roche Date: Wed, 9 Jul 2025 04:05:41 -0400 Subject: [PATCH 26/49] feat: use common annotation prefix to simplify filtering in informer transformers (#5621) In order to filter annotations in informer transformers, this PR makes it more explicit that a common prefix is used. New prefixes can be added later-on if need be, but all annotations should be anchored in a known prefix. Signed-off-by: Valerian Roche --- source/annotations/annotations.go | 40 +++++++++++++++++-------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/source/annotations/annotations.go b/source/annotations/annotations.go index bd21ca2bc..abfce1359 100644 --- a/source/annotations/annotations.go +++ b/source/annotations/annotations.go @@ -18,37 +18,41 @@ import ( ) const ( + // AnnotationKeyPrefix is set on all annotations consumed by external-dns (outside of user templates) + // to provide easy filtering. + AnnotationKeyPrefix = "external-dns.alpha.kubernetes.io/" + // CloudflareProxiedKey The annotation used for determining if traffic will go through Cloudflare - CloudflareProxiedKey = "external-dns.alpha.kubernetes.io/cloudflare-proxied" - CloudflareCustomHostnameKey = "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname" - CloudflareRegionKey = "external-dns.alpha.kubernetes.io/cloudflare-region-key" - CloudflareRecordCommentKey = "external-dns.alpha.kubernetes.io/cloudflare-record-comment" + CloudflareProxiedKey = AnnotationKeyPrefix + "cloudflare-proxied" + CloudflareCustomHostnameKey = AnnotationKeyPrefix + "cloudflare-custom-hostname" + CloudflareRegionKey = AnnotationKeyPrefix + "cloudflare-region-key" + CloudflareRecordCommentKey = AnnotationKeyPrefix + "cloudflare-record-comment" - AWSPrefix = "external-dns.alpha.kubernetes.io/aws-" - SCWPrefix = "external-dns.alpha.kubernetes.io/scw-" - WebhookPrefix = "external-dns.alpha.kubernetes.io/webhook-" - CloudflarePrefix = "external-dns.alpha.kubernetes.io/cloudflare-" + AWSPrefix = AnnotationKeyPrefix + "aws-" + SCWPrefix = AnnotationKeyPrefix + "scw-" + WebhookPrefix = AnnotationKeyPrefix + "webhook-" + CloudflarePrefix = AnnotationKeyPrefix + "cloudflare-" - TtlKey = "external-dns.alpha.kubernetes.io/ttl" + TtlKey = AnnotationKeyPrefix + "ttl" ttlMinimum = 1 ttlMaximum = math.MaxInt32 - SetIdentifierKey = "external-dns.alpha.kubernetes.io/set-identifier" - AliasKey = "external-dns.alpha.kubernetes.io/alias" - TargetKey = "external-dns.alpha.kubernetes.io/target" + SetIdentifierKey = AnnotationKeyPrefix + "set-identifier" + AliasKey = AnnotationKeyPrefix + "alias" + TargetKey = AnnotationKeyPrefix + "target" // The annotation used for figuring out which controller is responsible - ControllerKey = "external-dns.alpha.kubernetes.io/controller" + ControllerKey = AnnotationKeyPrefix + "controller" // The annotation used for defining the desired hostname - HostnameKey = "external-dns.alpha.kubernetes.io/hostname" + HostnameKey = AnnotationKeyPrefix + "hostname" // The annotation used for specifying whether the public or private interface address is used - AccessKey = "external-dns.alpha.kubernetes.io/access" + AccessKey = AnnotationKeyPrefix + "access" // The annotation used for specifying the type of endpoints to use for headless services - EndpointsTypeKey = "external-dns.alpha.kubernetes.io/endpoints-type" + EndpointsTypeKey = AnnotationKeyPrefix + "endpoints-type" // The annotation used to determine the source of hostnames for ingresses. This is an optional field - all // available hostname sources are used if not specified. - IngressHostnameSourceKey = "external-dns.alpha.kubernetes.io/ingress-hostname-source" + IngressHostnameSourceKey = AnnotationKeyPrefix + "ingress-hostname-source" // The value of the controller annotation so that we feel responsible ControllerValue = "dns-controller" // The annotation used for defining the desired hostname - InternalHostnameKey = "external-dns.alpha.kubernetes.io/internal-hostname" + InternalHostnameKey = AnnotationKeyPrefix + "internal-hostname" ) From a336ee0d96bec76359f1b776f254067d5384ca2b Mon Sep 17 00:00:00 2001 From: Ivan Ka <5395690+ivankatliarchuk@users.noreply.github.com> Date: Fri, 11 Jul 2025 16:47:28 +0100 Subject: [PATCH 27/49] chore(plan): added tests for cases with asterisks (#5640) * chore(plan): added tests for cases with asterisks Signed-off-by: ivan katliarchuk * chore(plan): added tests for cases with asterisks Signed-off-by: ivan katliarchuk --------- Signed-off-by: ivan katliarchuk --- plan/plan_test.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plan/plan_test.go b/plan/plan_test.go index c4278087e..10ce5a992 100644 --- a/plan/plan_test.go +++ b/plan/plan_test.go @@ -1109,6 +1109,14 @@ func TestNormalizeDNSName(tt *testing.T) { "xn--nordic--w1a.kitty😸.com.", "xn--nordic--w1a.xn--kitty-pd34d.com.", }, + { + "*.example.com.", + "*.example.com.", + }, + { + "*.example.com", + "*.example.com.", + }, } for _, r := range records { tt.Run(r.dnsName, func(t *testing.T) { From 71e368e70fb6e69baad13acdb02b6987580c5b94 Mon Sep 17 00:00:00 2001 From: chemi0213 Date: Sat, 12 Jul 2025 00:17:31 +0800 Subject: [PATCH 28/49] Added support for ap-east-2 (#5638) --- provider/aws/aws.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/provider/aws/aws.go b/provider/aws/aws.go index b27ba209d..a5796ae1a 100644 --- a/provider/aws/aws.go +++ b/provider/aws/aws.go @@ -86,6 +86,7 @@ var canonicalHostedZones = map[string]string{ "ca-central-1.elb.amazonaws.com": "ZQSVJUPU6J1EY", "ca-west-1.elb.amazonaws.com": "Z06473681N0SF6OS049SD", "ap-east-1.elb.amazonaws.com": "Z3DQVH9N71FHZ0", + "ap-east-2.elb.amazonaws.com": "Z02789141MW7T1WBU19PO", "ap-south-1.elb.amazonaws.com": "ZP97RAFLXTNZK", "ap-south-2.elb.amazonaws.com": "Z0173938T07WNTVAEPZN", "ap-northeast-2.elb.amazonaws.com": "ZWKZPGTI48KDX", @@ -123,6 +124,7 @@ var canonicalHostedZones = map[string]string{ "elb.ca-central-1.amazonaws.com": "Z2EPGBW3API2WT", "elb.ca-west-1.amazonaws.com": "Z02754302KBB00W2LKWZ9", "elb.ap-east-1.amazonaws.com": "Z12Y7K3UBGUAD1", + "elb.ap-east-2.amazonaws.com": "Z09176273OC2HWIAUNYW", "elb.ap-south-1.amazonaws.com": "ZVDDRBQ08TROA", "elb.ap-south-2.amazonaws.com": "Z0711778386UTO08407HT", "elb.ap-northeast-3.amazonaws.com": "Z1GWIQ4HH19I5X", @@ -161,6 +163,7 @@ var canonicalHostedZones = map[string]string{ "us-east-2.vpce.amazonaws.com": "ZC8PG0KIFKBRI", "af-south-1.vpce.amazonaws.com": "Z09302161J80N9A7UTP7U", "ap-east-1.vpce.amazonaws.com": "Z2LIHJ7PKBEMWN", + "ap-east-2.vpce.amazonaws.com": "Z09379811HWP0POAUWVN3", "ap-northeast-1.vpce.amazonaws.com": "Z2E726K9Y6RL4W", "ap-northeast-2.vpce.amazonaws.com": "Z27UANNT0PRK1T", "ap-northeast-3.vpce.amazonaws.com": "Z376B5OMM2JZL2", @@ -194,6 +197,7 @@ var canonicalHostedZones = map[string]string{ "execute-api.us-west-2.amazonaws.com": "Z2OJLYMUO9EFXC", "execute-api.af-south-1.amazonaws.com": "Z2DHW2332DAMTN", "execute-api.ap-east-1.amazonaws.com": "Z3FD1VL90ND7K5", + "execute-api.ap-east-2.amazonaws.com": "Z02909591O7FG9Q56HWB1", "execute-api.ap-south-1.amazonaws.com": "Z3VO1THU9YC4UR", "execute-api.ap-northeast-2.amazonaws.com": "Z20JF4UZKIW1U8", "execute-api.ap-southeast-1.amazonaws.com": "ZL327KTPIQFUL", From 385327e2e1af0e056e30b8169c0f1f9f947896f0 Mon Sep 17 00:00:00 2001 From: vkolobara Date: Fri, 11 Jul 2025 18:47:28 +0200 Subject: [PATCH 29/49] fix(pihole): create record for all targets (#5584) * fix(pihole): create record for all targets * fix(pihole): add multiple target logic to parent pihole provider * style(pihole): fix golangci-lint issues * fix(pihole): make listRecords return more than 1 target, improve dry run * test(pihole): listRecords test no longer depend on order * style(pihole): linter * test(pihole): more tests depending on order * test(pihole): add tests for v6 client * style(pihole): linter --- provider/pihole/clientV6.go | 77 ++++++++---- provider/pihole/clientV6_test.go | 194 ++++++++++++++++++++++++------- provider/pihole/pihole.go | 44 +++++-- provider/pihole/piholeV6_test.go | 16 ++- 4 files changed, 250 insertions(+), 81 deletions(-) diff --git a/provider/pihole/clientV6.go b/provider/pihole/clientV6.go index ebaaabd6a..0d220b03c 100644 --- a/provider/pihole/clientV6.go +++ b/provider/pihole/clientV6.go @@ -143,12 +143,13 @@ func isValidIPv6(ip string) bool { } func (p *piholeClientV6) listRecords(ctx context.Context, rtype string) ([]*endpoint.Endpoint, error) { - out := make([]*endpoint.Endpoint, 0) results, err := p.getConfigValue(ctx, rtype) if err != nil { return nil, err } + endpoints := make(map[string]*endpoint.Endpoint) + for _, rec := range results { recs := strings.FieldsFunc(rec, func(r rune) bool { return r == ' ' || r == ',' @@ -186,7 +187,18 @@ func (p *piholeClientV6) listRecords(ctx context.Context, rtype string) ([]*endp } } - out = append(out, endpoint.NewEndpointWithTTL(DNSName, rtype, Ttl, Target)) + ep := endpoint.NewEndpointWithTTL(DNSName, rtype, Ttl, Target) + + if oldEp, ok := endpoints[DNSName]; ok { + ep.Targets = append(oldEp.Targets, Target) + } + + endpoints[DNSName] = ep + } + + out := make([]*endpoint.Endpoint, 0, len(endpoints)) + for _, ep := range endpoints { + out = append(out, ep) } return out, nil } @@ -272,37 +284,44 @@ func (p *piholeClientV6) apply(ctx context.Context, action string, ep *endpoint. return nil } - if p.cfg.DryRun { - log.Infof("DRY RUN: %s %s IN %s -> %s", action, ep.DNSName, ep.RecordType, ep.Targets[0]) - return nil - } - - log.Infof("%s %s IN %s -> %s", action, ep.DNSName, ep.RecordType, ep.Targets[0]) - // Get the current record if strings.Contains(ep.DNSName, "*") { return provider.NewSoftError(errors.New("UNSUPPORTED: Pihole DNS names cannot return wildcard")) } - switch ep.RecordType { - case endpoint.RecordTypeA, endpoint.RecordTypeAAAA: - apiUrl = p.generateApiUrl(apiUrl, fmt.Sprintf("%s %s", ep.Targets, ep.DNSName)) - case endpoint.RecordTypeCNAME: - if ep.RecordTTL.IsConfigured() { - apiUrl = p.generateApiUrl(apiUrl, fmt.Sprintf("%s,%s,%d", ep.DNSName, ep.Targets, ep.RecordTTL)) - } else { - apiUrl = p.generateApiUrl(apiUrl, fmt.Sprintf("%s,%s", ep.DNSName, ep.Targets)) + if ep.RecordType == endpoint.RecordTypeCNAME && len(ep.Targets) > 1 { + return provider.NewSoftError(errors.New("UNSUPPORTED: Pihole CNAME records cannot have multiple targets")) + } + + for _, target := range ep.Targets { + if p.cfg.DryRun { + log.Infof("DRY RUN: %s %s IN %s -> %s", action, ep.DNSName, ep.RecordType, target) + continue } - } - req, err := http.NewRequestWithContext(ctx, action, apiUrl, nil) - if err != nil { - return err - } + log.Infof("%s %s IN %s -> %s", action, ep.DNSName, ep.RecordType, target) - _, err = p.do(req) - if err != nil { - return err + targetApiUrl := apiUrl + + switch ep.RecordType { + case endpoint.RecordTypeA, endpoint.RecordTypeAAAA: + targetApiUrl = p.generateApiUrl(targetApiUrl, fmt.Sprintf("%s %s", target, ep.DNSName)) + case endpoint.RecordTypeCNAME: + if ep.RecordTTL.IsConfigured() { + targetApiUrl = p.generateApiUrl(targetApiUrl, fmt.Sprintf("%s,%s,%d", ep.DNSName, target, ep.RecordTTL)) + } else { + targetApiUrl = p.generateApiUrl(targetApiUrl, fmt.Sprintf("%s,%s", ep.DNSName, target)) + } + } + req, err := http.NewRequestWithContext(ctx, action, targetApiUrl, nil) + if err != nil { + return err + } + + _, err = p.do(req) + if err != nil { + return err + } } return nil @@ -400,6 +419,14 @@ func (p *piholeClientV6) do(req *http.Request) ([]byte, error) { if err := json.Unmarshal(jRes, &apiError); err != nil { return nil, fmt.Errorf("failed to unmarshal error response: %w", err) } + // Ignore if the entry already exists when adding a record + if strings.Contains(apiError.Error.Message, "Item already present") { + return jRes, nil + } + // Ignore if the entry does not exist when deleting a record + if res.StatusCode == http.StatusNotFound && req.Method == http.MethodDelete { + return jRes, nil + } if log.IsLevelEnabled(log.DebugLevel) { log.Debugf("Error on request %s", req.URL) if req.Body != nil { diff --git a/provider/pihole/clientV6_test.go b/provider/pihole/clientV6_test.go index 255032dd0..d474f8eba 100644 --- a/provider/pihole/clientV6_test.go +++ b/provider/pihole/clientV6_test.go @@ -23,10 +23,10 @@ import ( "fmt" "net/http" "net/http/httptest" - "strconv" "strings" "testing" + "github.com/google/go-cmp/cmp" "sigs.k8s.io/external-dns/endpoint" ) @@ -192,10 +192,14 @@ func TestListRecordsV6(t *testing.T) { "192.168.178.33 service1.example.com", "192.168.178.34 service2.example.com", "192.168.178.34 service3.example.com", + "192.168.178.35 service8.example.com", + "192.168.178.36 service8.example.com", "fc00::1:192:168:1:1 service4.example.com", "fc00::1:192:168:1:2 service5.example.com", "fc00::1:192:168:1:3 service6.example.com", "::ffff:192.168.20.3 service7.example.com", + "fc00::1:192:168:1:4 service9.example.com", + "fc00::1:192:168:1:5 service9.example.com", "192.168.20.3 service7.example.com" ] } @@ -237,37 +241,70 @@ func TestListRecordsV6(t *testing.T) { } // Ensure A records were parsed correctly - expected := [][]string{ - {"service1.example.com", "192.168.178.33"}, - {"service2.example.com", "192.168.178.34"}, - {"service3.example.com", "192.168.178.34"}, - {"service7.example.com", "192.168.20.3"}, + expected := []*endpoint.Endpoint{ + { + DNSName: "service1.example.com", + Targets: []string{"192.168.178.33"}, + }, + { + DNSName: "service2.example.com", + Targets: []string{"192.168.178.34"}, + }, + { + DNSName: "service3.example.com", + Targets: []string{"192.168.178.34"}, + }, + { + DNSName: "service7.example.com", + Targets: []string{"192.168.20.3"}, + }, + { + DNSName: "service8.example.com", + Targets: []string{"192.168.178.35", "192.168.178.36"}, + }, } // Test retrieve A records unfiltered arecs, err := cl.listRecords(context.Background(), endpoint.RecordTypeA) if err != nil { t.Fatal(err) } - if len(arecs) != len(expected) { - t.Fatalf("Expected %d A records returned, got: %d", len(expected), len(arecs)) - } - for idx, rec := range arecs { - if rec.DNSName != expected[idx][0] { - t.Error("Got invalid DNS Name:", rec.DNSName, "expected:", expected[idx][0]) - } - if rec.Targets[0] != expected[idx][1] { - t.Error("Got invalid target:", rec.Targets[0], "expected:", expected[idx][1]) + expectedMap := make(map[string]*endpoint.Endpoint) + for _, ep := range expected { + expectedMap[ep.DNSName] = ep + } + for _, rec := range arecs { + if ep, ok := expectedMap[rec.DNSName]; ok { + if cmp.Diff(ep.Targets, rec.Targets) != "" { + t.Errorf("Got invalid targets for %s: %v, expected: %v", rec.DNSName, rec.Targets, ep.Targets) + } } } // Ensure AAAA records were parsed correctly - expected = [][]string{ - {"service4.example.com", "fc00::1:192:168:1:1"}, - {"service5.example.com", "fc00::1:192:168:1:2"}, - {"service6.example.com", "fc00::1:192:168:1:3"}, - {"service7.example.com", "::ffff:192.168.20.3"}, + expected = []*endpoint.Endpoint{ + { + DNSName: "service4.example.com", + Targets: []string{"fc00::1:192:168:1:1"}, + }, + { + DNSName: "service5.example.com", + Targets: []string{"fc00::1:192:168:1:2"}, + }, + { + DNSName: "service6.example.com", + Targets: []string{"fc00::1:192:168:1:3"}, + }, + { + DNSName: "service7.example.com", + Targets: []string{"::ffff:192.168.20.3"}, + }, + { + DNSName: "service9.example.com", + Targets: []string{"fc00::1:192:168:1:4", "fc00::1:192:168:1:5"}, + }, } + // Test retrieve AAAA records unfiltered arecs, err = cl.listRecords(context.Background(), endpoint.RecordTypeAAAA) if err != nil { @@ -278,20 +315,34 @@ func TestListRecordsV6(t *testing.T) { t.Fatalf("Expected %d AAAA records returned, got: %d", len(expected), len(arecs)) } - for idx, rec := range arecs { - if rec.DNSName != expected[idx][0] { - t.Error("Got invalid DNS Name:", rec.DNSName, "expected:", expected[idx][0]) - } - if rec.Targets[0] != expected[idx][1] { - t.Error("Got invalid target:", rec.Targets[0], "expected:", expected[idx][1]) + expectedMap = make(map[string]*endpoint.Endpoint) + for _, ep := range expected { + expectedMap[ep.DNSName] = ep + } + for _, rec := range arecs { + if ep, ok := expectedMap[rec.DNSName]; ok { + if cmp.Diff(ep.Targets, rec.Targets) != "" { + t.Errorf("Got invalid targets for %s: %v, expected: %v", rec.DNSName, rec.Targets, ep.Targets) + } } } // Ensure CNAME records were parsed correctly - expected = [][]string{ - {"source1.example.com", "target1.domain.com", "1000"}, - {"source2.example.com", "target2.domain.com", "50"}, - {"source3.example.com", "target3.domain.com"}, + expected = []*endpoint.Endpoint{ + { + DNSName: "source1.example.com", + Targets: []string{"target1.domain.com"}, + RecordTTL: 1000, + }, + { + DNSName: "source2.example.com", + Targets: []string{"target2.domain.com"}, + RecordTTL: 50, + }, + { + DNSName: "source3.example.com", + Targets: []string{"target3.domain.com"}, + }, } // Test retrieve CNAME records unfiltered @@ -303,17 +354,14 @@ func TestListRecordsV6(t *testing.T) { t.Fatalf("Expected %d CAME records returned, got: %d", len(expected), len(cnamerecs)) } - for idx, rec := range cnamerecs { - if rec.DNSName != expected[idx][0] { - t.Error("Got invalid DNS Name:", rec.DNSName, "expected:", expected[idx][0]) - } - if rec.Targets[0] != expected[idx][1] { - t.Error("Got invalid target:", rec.Targets[0], "expected:", expected[idx][1]) - } - if len(expected[idx]) == 3 { - expectedTTL, _ := strconv.ParseInt(expected[idx][2], 10, 64) - if int64(rec.RecordTTL) != expectedTTL { - t.Error("Got invalid TTL:", rec.RecordTTL, "expected:", expected[idx][2]) + expectedMap = make(map[string]*endpoint.Endpoint) + for _, ep := range expected { + expectedMap[ep.DNSName] = ep + } + for _, rec := range arecs { + if ep, ok := expectedMap[rec.DNSName]; ok { + if cmp.Diff(ep.Targets, rec.Targets) != "" { + t.Errorf("Got invalid targets for %s: %v, expected: %v", rec.DNSName, rec.Targets, ep.Targets) } } } @@ -432,8 +480,34 @@ func TestErrorsV6(t *testing.T) { if len(resp) != 2 { t.Fatal("Expected one records returned, got:", len(resp)) } - if resp[1].RecordTTL != 0 { - t.Fatal("Expected no TTL returned, got:", resp[0].RecordTTL) + + expected := []*endpoint.Endpoint{ + { + DNSName: "source1.example.com", + Targets: []string{"target1.domain.com"}, + RecordTTL: 100, + }, + { + DNSName: "source2.example.com", + Targets: []string{"target2.domain.com"}, + }, + } + + expectedMap := make(map[string]*endpoint.Endpoint) + for _, ep := range expected { + expectedMap[ep.DNSName] = ep + } + for _, rec := range resp { + if ep, ok := expectedMap[rec.DNSName]; ok { + if cmp.Diff(ep.Targets, rec.Targets) != "" { + t.Errorf("Got invalid targets for %s: %v, expected: %v", rec.DNSName, rec.Targets, ep.Targets) + } + if ep.RecordTTL != rec.RecordTTL { + t.Errorf("Got invalid TTL for %s: %d, expected: %d", rec.DNSName, rec.RecordTTL, ep.RecordTTL) + } + } else { + t.Errorf("Unexpected record found: %s", rec.DNSName) + } } } @@ -717,6 +791,10 @@ func TestCreateRecordV6(t *testing.T) { if r.Method == http.MethodPut && (r.URL.Path == "/api/config/dns/hosts/192.168.1.1 test.example.com" || r.URL.Path == "/api/config/dns/hosts/fc00::1:192:168:1:1 test.example.com" || r.URL.Path == "/api/config/dns/cnameRecords/source1.example.com,target1.domain.com" || + r.URL.Path == "/api/config/dns/hosts/192.168.1.2 test.example.com" || + r.URL.Path == "/api/config/dns/hosts/192.168.1.3 test.example.com" || + r.URL.Path == "/api/config/dns/hosts/fc00::1:192:168:1:2 test.example.com" || + r.URL.Path == "/api/config/dns/hosts/fc00::1:192:168:1:3 test.example.com" || r.URL.Path == "/api/config/dns/cnameRecords/source2.example.com,target2.domain.com,500") { // Return A records @@ -748,6 +826,16 @@ func TestCreateRecordV6(t *testing.T) { t.Fatal(err) } + // Test create multiple A records + ep = &endpoint.Endpoint{ + DNSName: "test.example.com", + Targets: []string{"192.168.1.2", "192.168.1.3"}, + RecordType: endpoint.RecordTypeA, + } + if err := cl.createRecord(context.Background(), ep); err != nil { + t.Fatal(err) + } + // Test create AAAA record ep = &endpoint.Endpoint{ DNSName: "test.example.com", @@ -758,6 +846,16 @@ func TestCreateRecordV6(t *testing.T) { t.Fatal(err) } + // Test create multiple AAAA records + ep = &endpoint.Endpoint{ + DNSName: "test.example.com", + Targets: []string{"fc00::1:192:168:1:2", "fc00::1:192:168:1:3"}, + RecordType: endpoint.RecordTypeAAAA, + } + if err := cl.createRecord(context.Background(), ep); err != nil { + t.Fatal(err) + } + // Test create CNAME record ep = &endpoint.Endpoint{ DNSName: "source1.example.com", @@ -779,6 +877,16 @@ func TestCreateRecordV6(t *testing.T) { t.Fatal(err) } + // Test create CNAME record with multiple targets and ensure it fails + ep = &endpoint.Endpoint{ + DNSName: "source3.example.com", + Targets: []string{"target3.domain.com", "target4.domain.com"}, + RecordType: endpoint.RecordTypeCNAME, + } + if err := cl.createRecord(context.Background(), ep); err == nil { + t.Fatal(err) + } + // Test create a wildcard record and ensure it fails ep = &endpoint.Endpoint{ DNSName: "*.example.com", diff --git a/provider/pihole/pihole.go b/provider/pihole/pihole.go index 53a7bf2af..cf401f374 100644 --- a/provider/pihole/pihole.go +++ b/provider/pihole/pihole.go @@ -19,6 +19,9 @@ package pihole import ( "context" "errors" + "slices" + + "github.com/google/go-cmp/cmp" "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" @@ -32,7 +35,8 @@ var ErrNoPiholeServer = errors.New("no pihole server found in the environment or // PiholeProvider is an implementation of Provider for Pi-hole Local DNS. type PiholeProvider struct { provider.BaseProvider - api piholeAPI + api piholeAPI + apiVersion string } // PiholeConfig is used for configuring a PiholeProvider. @@ -70,7 +74,7 @@ func NewPiholeProvider(cfg PiholeConfig) (*PiholeProvider, error) { if err != nil { return nil, err } - return &PiholeProvider{api: api}, nil + return &PiholeProvider{api: api, apiVersion: cfg.APIVersion}, nil } // Records implements Provider, populating a slice of endpoints from @@ -105,6 +109,19 @@ func (p *PiholeProvider) ApplyChanges(ctx context.Context, changes *plan.Changes updateNew := make(map[piholeEntryKey]*endpoint.Endpoint) for _, ep := range changes.UpdateNew { key := piholeEntryKey{ep.DNSName, ep.RecordType} + + // If the API version is 6, we need to handle multiple targets for the same DNS name. + if p.apiVersion == "6" { + if existing, ok := updateNew[key]; ok { + existing.Targets = append(existing.Targets, ep.Targets...) + + // Deduplicate targets + slices.Sort(existing.Targets) + existing.Targets = slices.Compact(existing.Targets) + + ep = existing + } + } updateNew[key] = ep } @@ -112,14 +129,23 @@ func (p *PiholeProvider) ApplyChanges(ctx context.Context, changes *plan.Changes // Check if this existing entry has an exact match for an updated entry and skip it if so. key := piholeEntryKey{ep.DNSName, ep.RecordType} if newRecord := updateNew[key]; newRecord != nil { - // PiHole only has a single target; no need to compare other fields. - if newRecord.Targets[0] == ep.Targets[0] { - delete(updateNew, key) - continue + // If the API version is 6, we need to handle multiple targets for the same DNS name. + if p.apiVersion == "6" { + if cmp.Diff(ep.Targets, newRecord.Targets) == "" { + delete(updateNew, key) + continue + } + } else { + // For API version <= 5, we only check the first target. + if newRecord.Targets[0] == ep.Targets[0] { + delete(updateNew, key) + continue + } + } + + if err := p.api.deleteRecord(ctx, ep); err != nil { + return err } - } - if err := p.api.deleteRecord(ctx, ep); err != nil { - return err } } diff --git a/provider/pihole/piholeV6_test.go b/provider/pihole/piholeV6_test.go index 22e0a2005..b14f77bc0 100644 --- a/provider/pihole/piholeV6_test.go +++ b/provider/pihole/piholeV6_test.go @@ -22,6 +22,7 @@ import ( "reflect" "testing" + "github.com/google/go-cmp/cmp" "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" ) @@ -60,7 +61,7 @@ func (t *testPiholeClientV6) createRecord(_ context.Context, ep *endpoint.Endpoi func (t *testPiholeClientV6) deleteRecord(_ context.Context, ep *endpoint.Endpoint) error { newEPs := make([]*endpoint.Endpoint, 0) for _, existing := range t.endpoints { - if existing.DNSName != ep.DNSName && existing.Targets[0] != ep.Targets[0] { + if existing.DNSName != ep.DNSName || cmp.Diff(existing.Targets, ep.Targets) != "" || existing.RecordType != ep.RecordType { newEPs = append(newEPs, existing) } } @@ -82,7 +83,8 @@ func (r *requestTrackerV6) clear() { func TestErrorHandling(t *testing.T) { requests := requestTrackerV6{} p := &PiholeProvider{ - api: &testPiholeClientV6{endpoints: make([]*endpoint.Endpoint, 0), requests: &requests}, + api: &testPiholeClientV6{endpoints: make([]*endpoint.Endpoint, 0), requests: &requests}, + apiVersion: "6", } p.api.(*testPiholeClientV6).trigger = "AERROR" @@ -121,7 +123,8 @@ func TestNewPiholeProviderV6(t *testing.T) { func TestProviderV6(t *testing.T) { requests := requestTrackerV6{} p := &PiholeProvider{ - api: &testPiholeClientV6{endpoints: make([]*endpoint.Endpoint, 0), requests: &requests}, + api: &testPiholeClientV6{endpoints: make([]*endpoint.Endpoint, 0), requests: &requests}, + apiVersion: "6", } records, err := p.Records(context.Background()) @@ -342,6 +345,11 @@ func TestProviderV6(t *testing.T) { Targets: []string{"10.0.0.1"}, RecordType: endpoint.RecordTypeA, }, + { + DNSName: "test2.example.com", + Targets: []string{"10.0.0.2"}, + RecordType: endpoint.RecordTypeA, + }, { DNSName: "test1.example.com", Targets: []string{"fc00::1:192:168:1:1"}, @@ -383,7 +391,7 @@ func TestProviderV6(t *testing.T) { expectedCreateA := endpoint.Endpoint{ DNSName: "test2.example.com", - Targets: []string{"10.0.0.1"}, + Targets: []string{"10.0.0.1", "10.0.0.2"}, RecordType: endpoint.RecordTypeA, } expectedDeleteA := endpoint.Endpoint{ From e2b56049f7d1b826b44c449aa3a4bf2364907d19 Mon Sep 17 00:00:00 2001 From: vflaux <38909103+vflaux@users.noreply.github.com> Date: Fri, 11 Jul 2025 18:47:36 +0200 Subject: [PATCH 30/49] chore(cloudflare): use lib v4 for regional services (#5609) * chore(cloudflare): add cloudflare v4 client * chrome(cloudflare): cli v4 for regional hostanmes --- go.mod | 5 + go.sum | 12 +++ provider/cloudflare/cloudflare.go | 32 +++++-- provider/cloudflare/cloudflare_regional.go | 89 +++++++++++------- .../cloudflare/cloudflare_regional_test.go | 42 +++++---- provider/cloudflare/pagination.go | 34 +++++++ provider/cloudflare/pagination_test.go | 94 +++++++++++++++++++ 7 files changed, 246 insertions(+), 62 deletions(-) create mode 100644 provider/cloudflare/pagination.go create mode 100644 provider/cloudflare/pagination_test.go diff --git a/go.mod b/go.mod index 77e12a811..a56ae2817 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require ( github.com/cenkalti/backoff/v5 v5.0.2 github.com/civo/civogo v0.6.1 github.com/cloudflare/cloudflare-go v0.115.0 + github.com/cloudflare/cloudflare-go/v4 v4.5.1 github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381 github.com/datawire/ambassador v1.12.4 github.com/denverdino/aliyungo v0.0.0-20230411124812-ab98a9173ace @@ -162,6 +163,10 @@ require ( github.com/sosodev/duration v1.3.1 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/stretchr/objx v0.5.2 // indirect + github.com/tidwall/gjson v1.14.4 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect github.com/vektah/gqlparser/v2 v2.5.26 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect diff --git a/go.sum b/go.sum index 9d8b33e67..0dcef98ed 100644 --- a/go.sum +++ b/go.sum @@ -186,6 +186,8 @@ github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/cloudflare-go v0.115.0 h1:84/dxeeXweCc0PN5Cto44iTA8AkG1fyT11yPO5ZB7sM= github.com/cloudflare/cloudflare-go v0.115.0/go.mod h1:Ds6urDwn/TF2uIU24mu7H91xkKP8gSAHxQ44DSZgVmU= +github.com/cloudflare/cloudflare-go/v4 v4.5.1 h1:ZQgQ7QO+M9rK0KYx1CmppuG15ZTYGHn8F9/Fh7mCuQQ= +github.com/cloudflare/cloudflare-go/v4 v4.5.1/go.mod h1:XcYpLe7Mf6FN87kXzEWVnJ6z+vskW/k6eUqgqfhFE9k= github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381 h1:rdRS5BT13Iae9ssvcslol66gfOOXjaLYwqerEn/cl9s= github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381/go.mod h1:e5+USP2j8Le2M0Jo3qKPFnNhuo1wueU4nWHCXBOfQ14= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= @@ -990,7 +992,17 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/transip/gotransip/v6 v6.26.0 h1:Aejfvh8rSp8Mj2GX/RpdBjMCv+Iy/DmgfNgczPDP550= diff --git a/provider/cloudflare/cloudflare.go b/provider/cloudflare/cloudflare.go index 930370089..21d4a945c 100644 --- a/provider/cloudflare/cloudflare.go +++ b/provider/cloudflare/cloudflare.go @@ -29,6 +29,9 @@ import ( "strings" "github.com/cloudflare/cloudflare-go" + cloudflarev4 "github.com/cloudflare/cloudflare-go/v4" + "github.com/cloudflare/cloudflare-go/v4/addressing" + "github.com/cloudflare/cloudflare-go/v4/option" log "github.com/sirupsen/logrus" "golang.org/x/net/publicsuffix" @@ -109,17 +112,18 @@ type cloudFlareDNS interface { CreateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDNSRecordParams) (cloudflare.DNSRecord, error) DeleteDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, recordID string) error UpdateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.UpdateDNSRecordParams) error - ListDataLocalizationRegionalHostnames(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.ListDataLocalizationRegionalHostnamesParams) ([]cloudflare.RegionalHostname, error) - CreateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDataLocalizationRegionalHostnameParams) error - UpdateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.UpdateDataLocalizationRegionalHostnameParams) error - DeleteDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, hostname string) error + ListDataLocalizationRegionalHostnames(ctx context.Context, params addressing.RegionalHostnameListParams) autoPager[addressing.RegionalHostnameListResponse] + CreateDataLocalizationRegionalHostname(ctx context.Context, params addressing.RegionalHostnameNewParams) error + UpdateDataLocalizationRegionalHostname(ctx context.Context, hostname string, params addressing.RegionalHostnameEditParams) error + DeleteDataLocalizationRegionalHostname(ctx context.Context, hostname string, params addressing.RegionalHostnameDeleteParams) error CustomHostnames(ctx context.Context, zoneID string, page int, filter cloudflare.CustomHostname) ([]cloudflare.CustomHostname, cloudflare.ResultInfo, error) DeleteCustomHostname(ctx context.Context, zoneID string, customHostnameID string) error CreateCustomHostname(ctx context.Context, zoneID string, ch cloudflare.CustomHostname) (*cloudflare.CustomHostnameResponse, error) } type zoneService struct { - service *cloudflare.API + service *cloudflare.API + serviceV4 *cloudflarev4.Client } func (z zoneService) ZoneIDByName(zoneName string) (string, error) { @@ -287,8 +291,9 @@ func NewCloudFlareProvider( ) (*CloudFlareProvider, error) { // initialize via chosen auth method and returns new API object var ( - config *cloudflare.API - err error + config *cloudflare.API + configV4 *cloudflarev4.Client + err error ) if os.Getenv("CF_API_TOKEN") != "" { token := os.Getenv("CF_API_TOKEN") @@ -300,8 +305,15 @@ func NewCloudFlareProvider( token = strings.TrimSpace(string(tokenBytes)) } config, err = cloudflare.NewWithAPIToken(token) + configV4 = cloudflarev4.NewClient( + option.WithAPIToken(token), + ) } else { config, err = cloudflare.New(os.Getenv("CF_API_KEY"), os.Getenv("CF_API_EMAIL")) + configV4 = cloudflarev4.NewClient( + option.WithAPIKey(os.Getenv("CF_API_KEY")), + option.WithAPIEmail(os.Getenv("CF_API_EMAIL")), + ) } if err != nil { return nil, fmt.Errorf("failed to initialize cloudflare provider: %w", err) @@ -312,7 +324,7 @@ func NewCloudFlareProvider( } return &CloudFlareProvider{ - Client: zoneService{config}, + Client: zoneService{config, configV4}, domainFilter: domainFilter, zoneIDFilter: zoneIDFilter, proxiedByDefault: proxiedByDefault, @@ -646,12 +658,12 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud return fmt.Errorf("failed to build desired regional hostnames: %w", err) } if len(desiredRegionalHostnames) > 0 { - regionalHostnames, err := p.listDataLocalisationRegionalHostnames(ctx, resourceContainer) + regionalHostnames, err := p.listDataLocalisationRegionalHostnames(ctx, zoneID) if err != nil { return fmt.Errorf("could not fetch regional hostnames from zone, %w", err) } regionalHostnamesChanges := regionalHostnamesChanges(desiredRegionalHostnames, regionalHostnames) - if !p.submitRegionalHostnameChanges(ctx, regionalHostnamesChanges, resourceContainer) { + if !p.submitRegionalHostnameChanges(ctx, zoneID, regionalHostnamesChanges) { failedChange = true } } diff --git a/provider/cloudflare/cloudflare_regional.go b/provider/cloudflare/cloudflare_regional.go index 9b7b9f988..9502bcc92 100644 --- a/provider/cloudflare/cloudflare_regional.go +++ b/provider/cloudflare/cloudflare_regional.go @@ -22,7 +22,9 @@ import ( "maps" "slices" - cloudflare "github.com/cloudflare/cloudflare-go" + cloudflarev4 "github.com/cloudflare/cloudflare-go/v4" + "github.com/cloudflare/cloudflare-go/v4/addressing" + log "github.com/sirupsen/logrus" "sigs.k8s.io/external-dns/endpoint" @@ -53,46 +55,62 @@ type regionalHostnameChange struct { regionalHostname } -func (z zoneService) ListDataLocalizationRegionalHostnames(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.ListDataLocalizationRegionalHostnamesParams) ([]cloudflare.RegionalHostname, error) { - return z.service.ListDataLocalizationRegionalHostnames(ctx, rc, rp) +func (z zoneService) ListDataLocalizationRegionalHostnames(ctx context.Context, params addressing.RegionalHostnameListParams) autoPager[addressing.RegionalHostnameListResponse] { + return z.serviceV4.Addressing.RegionalHostnames.ListAutoPaging(ctx, params) } -func (z zoneService) CreateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDataLocalizationRegionalHostnameParams) error { - _, err := z.service.CreateDataLocalizationRegionalHostname(ctx, rc, rp) +func (z zoneService) CreateDataLocalizationRegionalHostname(ctx context.Context, params addressing.RegionalHostnameNewParams) error { + _, err := z.serviceV4.Addressing.RegionalHostnames.New(ctx, params) return err } -func (z zoneService) UpdateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.UpdateDataLocalizationRegionalHostnameParams) error { - _, err := z.service.UpdateDataLocalizationRegionalHostname(ctx, rc, rp) +func (z zoneService) UpdateDataLocalizationRegionalHostname(ctx context.Context, hostname string, params addressing.RegionalHostnameEditParams) error { + _, err := z.serviceV4.Addressing.RegionalHostnames.Edit(ctx, hostname, params) return err } -func (z zoneService) DeleteDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, hostname string) error { - return z.service.DeleteDataLocalizationRegionalHostname(ctx, rc, hostname) +func (z zoneService) DeleteDataLocalizationRegionalHostname(ctx context.Context, hostname string, params addressing.RegionalHostnameDeleteParams) error { + _, err := z.serviceV4.Addressing.RegionalHostnames.Delete(ctx, hostname, params) + return err +} + +// listDataLocalizationRegionalHostnamesParams is a function that returns the appropriate RegionalHostname List Param based on the zoneID +func listDataLocalizationRegionalHostnamesParams(zoneID string) addressing.RegionalHostnameListParams { + return addressing.RegionalHostnameListParams{ + ZoneID: cloudflarev4.F(zoneID), + } } // createDataLocalizationRegionalHostnameParams is a function that returns the appropriate RegionalHostname Param based on the cloudFlareChange passed in -func createDataLocalizationRegionalHostnameParams(rhc regionalHostnameChange) cloudflare.CreateDataLocalizationRegionalHostnameParams { - return cloudflare.CreateDataLocalizationRegionalHostnameParams{ - Hostname: rhc.hostname, - RegionKey: rhc.regionKey, +func createDataLocalizationRegionalHostnameParams(zoneID string, rhc regionalHostnameChange) addressing.RegionalHostnameNewParams { + return addressing.RegionalHostnameNewParams{ + ZoneID: cloudflarev4.F(zoneID), + Hostname: cloudflarev4.F(rhc.hostname), + RegionKey: cloudflarev4.F(rhc.regionKey), } } // updateDataLocalizationRegionalHostnameParams is a function that returns the appropriate RegionalHostname Param based on the cloudFlareChange passed in -func updateDataLocalizationRegionalHostnameParams(rhc regionalHostnameChange) cloudflare.UpdateDataLocalizationRegionalHostnameParams { - return cloudflare.UpdateDataLocalizationRegionalHostnameParams{ - Hostname: rhc.hostname, - RegionKey: rhc.regionKey, +func updateDataLocalizationRegionalHostnameParams(zoneID string, rhc regionalHostnameChange) addressing.RegionalHostnameEditParams { + return addressing.RegionalHostnameEditParams{ + ZoneID: cloudflarev4.F(zoneID), + RegionKey: cloudflarev4.F(rhc.regionKey), + } +} + +// deleteDataLocalizationRegionalHostnameParams is a function that returns the appropriate RegionalHostname Param based on the cloudFlareChange passed in +func deleteDataLocalizationRegionalHostnameParams(zoneID string, rhc regionalHostnameChange) addressing.RegionalHostnameDeleteParams { + return addressing.RegionalHostnameDeleteParams{ + ZoneID: cloudflarev4.F(zoneID), } } // submitRegionalHostnameChanges applies a set of regional hostname changes, returns false if at least one fails -func (p *CloudFlareProvider) submitRegionalHostnameChanges(ctx context.Context, rhChanges []regionalHostnameChange, resourceContainer *cloudflare.ResourceContainer) bool { +func (p *CloudFlareProvider) submitRegionalHostnameChanges(ctx context.Context, zoneID string, rhChanges []regionalHostnameChange) bool { failedChange := false for _, rhChange := range rhChanges { - if !p.submitRegionalHostnameChange(ctx, rhChange, resourceContainer) { + if !p.submitRegionalHostnameChange(ctx, zoneID, rhChange) { failedChange = true } } @@ -101,12 +119,12 @@ func (p *CloudFlareProvider) submitRegionalHostnameChanges(ctx context.Context, } // submitRegionalHostnameChange applies a single regional hostname change, returns false if it fails -func (p *CloudFlareProvider) submitRegionalHostnameChange(ctx context.Context, rhChange regionalHostnameChange, resourceContainer *cloudflare.ResourceContainer) bool { +func (p *CloudFlareProvider) submitRegionalHostnameChange(ctx context.Context, zoneID string, rhChange regionalHostnameChange) bool { changeLog := log.WithFields(log.Fields{ "hostname": rhChange.hostname, "region_key": rhChange.regionKey, "action": rhChange.action.String(), - "zone": resourceContainer.Identifier, + "zone": zoneID, }) if p.DryRun { changeLog.Debug("Dry run: skipping regional hostname change", rhChange.action) @@ -115,21 +133,22 @@ func (p *CloudFlareProvider) submitRegionalHostnameChange(ctx context.Context, r switch rhChange.action { case cloudFlareCreate: changeLog.Debug("Creating regional hostname") - regionalHostnameParam := createDataLocalizationRegionalHostnameParams(rhChange) - if err := p.Client.CreateDataLocalizationRegionalHostname(ctx, resourceContainer, regionalHostnameParam); err != nil { + params := createDataLocalizationRegionalHostnameParams(zoneID, rhChange) + if err := p.Client.CreateDataLocalizationRegionalHostname(ctx, params); err != nil { changeLog.Errorf("failed to create regional hostname: %v", err) return false } case cloudFlareUpdate: changeLog.Debug("Updating regional hostname") - regionalHostnameParam := updateDataLocalizationRegionalHostnameParams(rhChange) - if err := p.Client.UpdateDataLocalizationRegionalHostname(ctx, resourceContainer, regionalHostnameParam); err != nil { + params := updateDataLocalizationRegionalHostnameParams(zoneID, rhChange) + if err := p.Client.UpdateDataLocalizationRegionalHostname(ctx, rhChange.hostname, params); err != nil { changeLog.Errorf("failed to update regional hostname: %v", err) return false } case cloudFlareDelete: changeLog.Debug("Deleting regional hostname") - if err := p.Client.DeleteDataLocalizationRegionalHostname(ctx, resourceContainer, rhChange.hostname); err != nil { + params := deleteDataLocalizationRegionalHostnameParams(zoneID, rhChange) + if err := p.Client.DeleteDataLocalizationRegionalHostname(ctx, rhChange.hostname, params); err != nil { changeLog.Errorf("failed to delete regional hostname: %v", err) return false } @@ -137,18 +156,22 @@ func (p *CloudFlareProvider) submitRegionalHostnameChange(ctx context.Context, r return true } -func (p *CloudFlareProvider) listDataLocalisationRegionalHostnames(ctx context.Context, resourceContainer *cloudflare.ResourceContainer) (regionalHostnamesMap, error) { - rhs, err := p.Client.ListDataLocalizationRegionalHostnames(ctx, resourceContainer, cloudflare.ListDataLocalizationRegionalHostnamesParams{}) - if err != nil { - return nil, convertCloudflareError(err) - } +// listDataLocalisationRegionalHostnames fetches the current regional hostnames for the given zone ID. +// +// It returns a map of hostnames to regional hostnames, or an error if the request fails. +func (p *CloudFlareProvider) listDataLocalisationRegionalHostnames(ctx context.Context, zoneID string) (regionalHostnamesMap, error) { + params := listDataLocalizationRegionalHostnamesParams(zoneID) + iter := p.Client.ListDataLocalizationRegionalHostnames(ctx, params) rhsMap := make(regionalHostnamesMap) - for _, rh := range rhs { + for rh := range autoPagerIterator(iter) { rhsMap[rh.Hostname] = regionalHostname{ hostname: rh.Hostname, regionKey: rh.RegionKey, } } + if iter.Err() != nil { + return nil, convertCloudflareError(iter.Err()) + } return rhsMap, nil } @@ -193,7 +216,7 @@ func (p *CloudFlareProvider) addEnpointsProviderSpecificRegionKeyProperty(ctx co return nil } - regionalHostnames, err := p.listDataLocalisationRegionalHostnames(ctx, cloudflare.ZoneIdentifier(zoneID)) + regionalHostnames, err := p.listDataLocalisationRegionalHostnames(ctx, zoneID) if err != nil { return err } diff --git a/provider/cloudflare/cloudflare_regional_test.go b/provider/cloudflare/cloudflare_regional_test.go index 1aafbebac..aee88dfe0 100644 --- a/provider/cloudflare/cloudflare_regional_test.go +++ b/provider/cloudflare/cloudflare_regional_test.go @@ -24,6 +24,7 @@ import ( "testing" cloudflare "github.com/cloudflare/cloudflare-go" + "github.com/cloudflare/cloudflare-go/v4/addressing" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" @@ -33,61 +34,64 @@ import ( "sigs.k8s.io/external-dns/plan" ) -func (m *mockCloudFlareClient) ListDataLocalizationRegionalHostnames(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.ListDataLocalizationRegionalHostnamesParams) ([]cloudflare.RegionalHostname, error) { - if strings.Contains(rc.Identifier, "rherror") { - return nil, fmt.Errorf("failed to list regional hostnames") +func (m *mockCloudFlareClient) ListDataLocalizationRegionalHostnames(ctx context.Context, params addressing.RegionalHostnameListParams) autoPager[addressing.RegionalHostnameListResponse] { + zoneID := params.ZoneID.Value + if strings.Contains(zoneID, "rherror") { + return &mockAutoPager[addressing.RegionalHostnameListResponse]{err: fmt.Errorf("failed to list regional hostnames")} } - rhs := make([]cloudflare.RegionalHostname, 0, len(m.regionalHostnames[rc.Identifier])) - for _, rh := range m.regionalHostnames[rc.Identifier] { - rhs = append(rhs, cloudflare.RegionalHostname{ + results := make([]addressing.RegionalHostnameListResponse, 0, len(m.regionalHostnames[zoneID])) + for _, rh := range m.regionalHostnames[zoneID] { + results = append(results, addressing.RegionalHostnameListResponse{ Hostname: rh.hostname, RegionKey: rh.regionKey, }) } - return rhs, nil + return &mockAutoPager[addressing.RegionalHostnameListResponse]{ + items: results, + } } -func (m *mockCloudFlareClient) CreateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDataLocalizationRegionalHostnameParams) error { - if strings.Contains(rp.Hostname, "rherror") { +func (m *mockCloudFlareClient) CreateDataLocalizationRegionalHostname(ctx context.Context, params addressing.RegionalHostnameNewParams) error { + if strings.Contains(params.Hostname.Value, "rherror") { return fmt.Errorf("failed to create regional hostname") } m.Actions = append(m.Actions, MockAction{ Name: "CreateDataLocalizationRegionalHostname", - ZoneId: rc.Identifier, + ZoneId: params.ZoneID.Value, RecordId: "", RegionalHostname: regionalHostname{ - hostname: rp.Hostname, - regionKey: rp.RegionKey, + hostname: params.Hostname.Value, + regionKey: params.RegionKey.Value, }, }) return nil } -func (m *mockCloudFlareClient) UpdateDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.UpdateDataLocalizationRegionalHostnameParams) error { - if strings.Contains(rp.Hostname, "rherror") { +func (m *mockCloudFlareClient) UpdateDataLocalizationRegionalHostname(ctx context.Context, hostname string, params addressing.RegionalHostnameEditParams) error { + if strings.Contains(hostname, "rherror") { return fmt.Errorf("failed to update regional hostname") } m.Actions = append(m.Actions, MockAction{ Name: "UpdateDataLocalizationRegionalHostname", - ZoneId: rc.Identifier, + ZoneId: params.ZoneID.Value, RecordId: "", RegionalHostname: regionalHostname{ - hostname: rp.Hostname, - regionKey: rp.RegionKey, + hostname: hostname, + regionKey: params.RegionKey.Value, }, }) return nil } -func (m *mockCloudFlareClient) DeleteDataLocalizationRegionalHostname(ctx context.Context, rc *cloudflare.ResourceContainer, hostname string) error { +func (m *mockCloudFlareClient) DeleteDataLocalizationRegionalHostname(ctx context.Context, hostname string, params addressing.RegionalHostnameDeleteParams) error { if strings.Contains(hostname, "rherror") { return fmt.Errorf("failed to delete regional hostname") } m.Actions = append(m.Actions, MockAction{ Name: "DeleteDataLocalizationRegionalHostname", - ZoneId: rc.Identifier, + ZoneId: params.ZoneID.Value, RecordId: "", RegionalHostname: regionalHostname{ hostname: hostname, diff --git a/provider/cloudflare/pagination.go b/provider/cloudflare/pagination.go new file mode 100644 index 000000000..2b8337ae2 --- /dev/null +++ b/provider/cloudflare/pagination.go @@ -0,0 +1,34 @@ +/* +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 cloudflare + +type autoPager[T any] interface { + Next() bool + Current() T + Err() error +} + +// autoPagerIterator returns an iterator over an autoPager. +func autoPagerIterator[T any](iter autoPager[T]) func(yield func(T) bool) { + return func(yield func(T) bool) { + for iter.Next() { + if !yield(iter.Current()) { + return + } + } + } +} diff --git a/provider/cloudflare/pagination_test.go b/provider/cloudflare/pagination_test.go new file mode 100644 index 000000000..1f0c28e82 --- /dev/null +++ b/provider/cloudflare/pagination_test.go @@ -0,0 +1,94 @@ +/* +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 cloudflare + +import ( + "errors" + "slices" + "testing" + + "github.com/stretchr/testify/assert" +) + +type mockAutoPager[T any] struct { + items []T + index int + err error + errIndex int +} + +func (m *mockAutoPager[T]) Next() bool { + m.index++ + return !m.hasError() && m.hasNext() +} + +func (m *mockAutoPager[T]) Current() T { + if m.hasNext() && !m.hasError() { + return m.items[m.index-1] + } + var zero T + return zero +} + +func (m *mockAutoPager[T]) Err() error { + return m.err +} + +func (m *mockAutoPager[T]) hasError() bool { + return m.err != nil && m.errIndex <= m.index +} + +func (m *mockAutoPager[T]) hasNext() bool { + return m.index > 0 && m.index <= len(m.items) +} + +func TestAutoPagerIterator(t *testing.T) { + t.Run("iterate empty", func(t *testing.T) { + pager := &mockAutoPager[string]{} + iterator := autoPagerIterator(pager) + collected := slices.Collect(iterator) + assert.Empty(t, collected) + }) + + t.Run("iterate all items", func(t *testing.T) { + pager := &mockAutoPager[int]{items: []int{1, 2, 3, 4, 5}} + iterator := autoPagerIterator(pager) + collected := slices.Collect(iterator) + assert.Equal(t, []int{1, 2, 3, 4, 5}, collected) + }) + + t.Run("iterate with early termination", func(t *testing.T) { + pager := &mockAutoPager[int]{items: []int{1, 2, 3, 4, 5}} + iterator := autoPagerIterator(pager) + var collected []int + for item := range iterator { + collected = append(collected, item) + if item == 3 { + break + } + } + assert.Equal(t, []int{1, 2, 3}, collected) + }) + + t.Run("iterate with error at index", func(t *testing.T) { + expectedErr := errors.New("pager error") + pager := &mockAutoPager[int]{items: []int{1, 2, 3, 4, 5}, err: expectedErr, errIndex: 3} + iterator := autoPagerIterator(pager) + collected := slices.Collect(iterator) + assert.Equal(t, []int{1, 2}, collected) + }) +} From 8088b57dd1e07e6012a048472950ac71d19c5519 Mon Sep 17 00:00:00 2001 From: Raghu Date: Fri, 11 Jul 2025 22:17:44 +0530 Subject: [PATCH 31/49] docs(aws): add helm repo command to the tutorial (#5618) * added helm repo add command to the aws tutorial * Update docs/tutorials/aws.md Co-authored-by: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com> --------- Co-authored-by: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com> --- docs/tutorials/aws.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/tutorials/aws.md b/docs/tutorials/aws.md index 141ed7d1c..6585aa9e3 100644 --- a/docs/tutorials/aws.md +++ b/docs/tutorials/aws.md @@ -473,6 +473,8 @@ env: Finally, install the ExternalDNS chart with Helm using the configuration specified in your values.yaml file: ```shell +helm repo add --force-update external-dns https://kubernetes-sigs.github.io/external-dns/ + helm upgrade --install external-dns external-dns/external-dns --values values.yaml ``` From 28d0ff93168d9297f4ec2523ec5f4934b9b16637 Mon Sep 17 00:00:00 2001 From: Ivan Ka <5395690+ivankatliarchuk@users.noreply.github.com> Date: Fri, 11 Jul 2025 17:47:51 +0100 Subject: [PATCH 32/49] chore(source/net-filter): improve flow logic and add more tests (#5629) Signed-off-by: ivan katliarchuk --- endpoint/domain_filter_test.go | 6 + endpoint/target_filter.go | 20 +-- endpoint/target_filter_test.go | 33 ++++- source/wrappers/targetfiltersource.go | 20 ++- source/wrappers/targetfiltersource_test.go | 155 ++++++++++++++++++--- 5 files changed, 199 insertions(+), 35 deletions(-) diff --git a/endpoint/domain_filter_test.go b/endpoint/domain_filter_test.go index 29dfdfea6..328743464 100644 --- a/endpoint/domain_filter_test.go +++ b/endpoint/domain_filter_test.go @@ -949,3 +949,9 @@ func TestDomainFilterNormalizeDomain(t *testing.T) { assert.Equal(t, r.expect, gotName) } } + +func TestMatchTargetFilterReturnsProperEmptyVal(t *testing.T) { + var emptyFilters []string + assert.True(t, matchFilter(emptyFilters, "sometarget.com", true)) + assert.False(t, matchFilter(emptyFilters, "sometarget.com", false)) +} diff --git a/endpoint/target_filter.go b/endpoint/target_filter.go index e4e69957f..2706155e9 100644 --- a/endpoint/target_filter.go +++ b/endpoint/target_filter.go @@ -26,12 +26,13 @@ import ( // TargetFilterInterface defines the interface to select matching targets for a specific provider or runtime type TargetFilterInterface interface { Match(target string) bool + IsEnabled() bool } // TargetNetFilter holds a lists of valid target names type TargetNetFilter struct { - // FilterNets define what targets to match - FilterNets []*net.IPNet + // filterNets define what targets to match + filterNets []*net.IPNet // excludeNets define what targets not to match excludeNets []*net.IPNet } @@ -42,11 +43,9 @@ func prepareTargetFilters(filters []string) []*net.IPNet { for _, filter := range filters { filter = strings.TrimSpace(filter) - _, filterNet, err := net.ParseCIDR(filter) if err != nil { log.Errorf("Invalid target net filter: %s", filter) - continue } @@ -57,12 +56,17 @@ func prepareTargetFilters(filters []string) []*net.IPNet { // NewTargetNetFilterWithExclusions returns a new TargetNetFilter, given a list of matches and exclusions func NewTargetNetFilterWithExclusions(targetFilterNets []string, excludeNets []string) TargetNetFilter { - return TargetNetFilter{FilterNets: prepareTargetFilters(targetFilterNets), excludeNets: prepareTargetFilters(excludeNets)} + return TargetNetFilter{filterNets: prepareTargetFilters(targetFilterNets), excludeNets: prepareTargetFilters(excludeNets)} } // Match checks whether a target can be found in the TargetNetFilter. func (tf TargetNetFilter) Match(target string) bool { - return matchTargetNetFilter(tf.FilterNets, target, true) && !matchTargetNetFilter(tf.excludeNets, target, false) + return matchTargetNetFilter(tf.filterNets, target, true) && !matchTargetNetFilter(tf.excludeNets, target, false) +} + +// IsEnabled returns true if any filters or exclusions are set. +func (tf TargetNetFilter) IsEnabled() bool { + return len(tf.filterNets) > 0 || len(tf.excludeNets) > 0 } // matchTargetNetFilter determines if any `filters` match `target`. @@ -73,9 +77,9 @@ func matchTargetNetFilter(filters []*net.IPNet, target string, emptyval bool) bo return emptyval } - for _, filter := range filters { - ip := net.ParseIP(target) + ip := net.ParseIP(target) + for _, filter := range filters { if filter.Contains(ip) { return true } diff --git a/endpoint/target_filter_test.go b/endpoint/target_filter_test.go index 01ffbf5cf..d803093c1 100644 --- a/endpoint/target_filter_test.go +++ b/endpoint/target_filter_test.go @@ -66,6 +66,18 @@ var targetFilterTests = []targetFilterTest{ []string{"10.1.2.3"}, false, }, + { + []string{}, + []string{"10.0.0.0/8"}, + []string{"49.13.41.161"}, + true, + }, + { + []string{}, + []string{"10.0.0.0/8"}, + []string{"10.0.1.101"}, + false, + }, } func TestTargetFilterWithExclusions(t *testing.T) { @@ -89,8 +101,21 @@ func TestTargetFilterMatchWithEmptyFilter(t *testing.T) { } } -func TestMatchTargetFilterReturnsProperEmptyVal(t *testing.T) { - emptyFilters := []string{} - assert.True(t, matchFilter(emptyFilters, "sometarget.com", true)) - assert.False(t, matchFilter(emptyFilters, "sometarget.com", false)) +func TestTargetNetFilter_IsEnabled(t *testing.T) { + tests := []struct { + name string + filterNets []string + excludeNets []string + want bool + }{ + {"both empty", []string{}, []string{}, false}, + {"filterNets non-empty", []string{"10.0.0.0/8"}, []string{}, true}, + {"excludeNets non-empty", []string{}, []string{"10.0.0.0/8"}, true}, + {"both non-empty", []string{"10.0.0.0/8"}, []string{"192.168.0.0/16"}, true}, + } + + for _, tt := range tests { + tf := NewTargetNetFilterWithExclusions(tt.filterNets, tt.excludeNets) + assert.Equal(t, tt.want, tf.IsEnabled()) + } } diff --git a/source/wrappers/targetfiltersource.go b/source/wrappers/targetfiltersource.go index fe6d1d283..afc654d90 100644 --- a/source/wrappers/targetfiltersource.go +++ b/source/wrappers/targetfiltersource.go @@ -21,34 +21,38 @@ import ( log "github.com/sirupsen/logrus" - source2 "sigs.k8s.io/external-dns/source" + "sigs.k8s.io/external-dns/source" "sigs.k8s.io/external-dns/endpoint" ) // targetFilterSource is a Source that removes endpoints matching the target filter from its wrapped source. type targetFilterSource struct { - source source2.Source + source source.Source targetFilter endpoint.TargetFilterInterface } // NewTargetFilterSource creates a new targetFilterSource wrapping the provided Source. -func NewTargetFilterSource(source source2.Source, targetFilter endpoint.TargetFilterInterface) source2.Source { +func NewTargetFilterSource(source source.Source, targetFilter endpoint.TargetFilterInterface) source.Source { return &targetFilterSource{source: source, targetFilter: targetFilter} } // Endpoints collects endpoints from its wrapped source and returns // them without targets matching the target filter. func (ms *targetFilterSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) { - result := []*endpoint.Endpoint{} - endpoints, err := ms.source.Endpoints(ctx) if err != nil { return nil, err } + if !ms.targetFilter.IsEnabled() { + return endpoints, nil + } + + result := make([]*endpoint.Endpoint, 0, len(endpoints)) + for _, ep := range endpoints { - filteredTargets := []string{} + filteredTargets := make([]string, 0, len(ep.Targets)) for _, t := range ep.Targets { if ms.targetFilter.Match(t) { @@ -71,5 +75,7 @@ func (ms *targetFilterSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoi } func (ms *targetFilterSource) AddEventHandler(ctx context.Context, handler func()) { - ms.source.AddEventHandler(ctx, handler) + if ms.targetFilter.IsEnabled() { + ms.source.AddEventHandler(ctx, handler) + } } diff --git a/source/wrappers/targetfiltersource_test.go b/source/wrappers/targetfiltersource_test.go index 733c1e1a3..e0e01d745 100644 --- a/source/wrappers/targetfiltersource_test.go +++ b/source/wrappers/targetfiltersource_test.go @@ -19,6 +19,7 @@ package wrappers import ( "testing" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "golang.org/x/net/context" "sigs.k8s.io/external-dns/source" @@ -42,15 +43,21 @@ func (m *mockTargetNetFilter) Match(target string) bool { return m.targets[target] } +func (m *mockTargetNetFilter) IsEnabled() bool { + return true +} + // echoSource is a Source that returns the endpoints passed in on creation. type echoSource struct { + mock.Mock endpoints []*endpoint.Endpoint } func (e *echoSource) AddEventHandler(ctx context.Context, handler func()) { + e.Called(ctx) } -// Endpoints returns all of the endpoints passed in on creation +// Endpoints returns all the endpoints passed in on creation func (e *echoSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) { return e.endpoints, nil } @@ -63,7 +70,7 @@ func NewEchoSource(endpoints []*endpoint.Endpoint) source.Source { func TestEchoSourceReturnGivenSources(t *testing.T) { startEndpoints := []*endpoint.Endpoint{{ DNSName: "foo.bar.com", - RecordType: "A", + RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}, RecordTTL: endpoint.TTL(300), Labels: endpoint.Labels{}, @@ -75,9 +82,9 @@ func TestEchoSourceReturnGivenSources(t *testing.T) { t.Errorf("Expected no error but got %s", err.Error()) } - for i, endpoint := range endpoints { - if endpoint != startEndpoints[i] { - t.Errorf("Expected %s but got %s", startEndpoints[i], endpoint) + for i, ep := range endpoints { + if ep != startEndpoints[i] { + t.Errorf("Expected %s but got %s", startEndpoints[i], ep) } } } @@ -107,28 +114,28 @@ func TestTargetFilterSourceEndpoints(t *testing.T) { title: "filter exclusion all", filters: NewMockTargetNetFilter([]string{}), endpoints: []*endpoint.Endpoint{ - endpoint.NewEndpoint("foo", "A", "1.2.3.4"), - endpoint.NewEndpoint("foo", "A", "1.2.3.5"), - endpoint.NewEndpoint("foo", "A", "1.2.3.6"), - endpoint.NewEndpoint("foo", "A", "1.3.4.5"), - endpoint.NewEndpoint("foo", "A", "1.4.4.5")}, + endpoint.NewEndpoint("foo", endpoint.RecordTypeA, "1.2.3.4"), + endpoint.NewEndpoint("foo", endpoint.RecordTypeA, "1.2.3.5"), + endpoint.NewEndpoint("foo", endpoint.RecordTypeA, "1.2.3.6"), + endpoint.NewEndpoint("foo", endpoint.RecordTypeA, "1.3.4.5"), + endpoint.NewEndpoint("foo", endpoint.RecordTypeA, "1.4.4.5")}, expected: []*endpoint.Endpoint{}, }, { title: "filter exclude internal net", filters: NewMockTargetNetFilter([]string{"8.8.8.8"}), endpoints: []*endpoint.Endpoint{ - endpoint.NewEndpoint("foo", "A", "10.0.0.1"), - endpoint.NewEndpoint("foo", "A", "8.8.8.8")}, - expected: []*endpoint.Endpoint{endpoint.NewEndpoint("foo", "A", "8.8.8.8")}, + endpoint.NewEndpoint("foo", endpoint.RecordTypeA, "10.0.0.1"), + endpoint.NewEndpoint("foo", endpoint.RecordTypeA, "8.8.8.8")}, + expected: []*endpoint.Endpoint{endpoint.NewEndpoint("foo", endpoint.RecordTypeA, "8.8.8.8")}, }, { title: "filter only internal", filters: NewMockTargetNetFilter([]string{"10.0.0.1"}), endpoints: []*endpoint.Endpoint{ - endpoint.NewEndpoint("foo", "A", "10.0.0.1"), - endpoint.NewEndpoint("foo", "A", "8.8.8.8")}, - expected: []*endpoint.Endpoint{endpoint.NewEndpoint("foo", "A", "10.0.0.1")}, + endpoint.NewEndpoint("foo", endpoint.RecordTypeA, "10.0.0.1"), + endpoint.NewEndpoint("foo", endpoint.RecordTypeA, "8.8.8.8")}, + expected: []*endpoint.Endpoint{endpoint.NewEndpoint("foo", endpoint.RecordTypeA, "10.0.0.1")}, }, } for _, tt := range tests { @@ -145,3 +152,119 @@ func TestTargetFilterSourceEndpoints(t *testing.T) { }) } } + +func TestTargetFilterConcreteTargetFilter(t *testing.T) { + tests := []struct { + title string + filters endpoint.TargetFilterInterface + endpoints []*endpoint.Endpoint + expected []*endpoint.Endpoint + }{ + { + title: "should skip filtering if no filters are set", + filters: endpoint.NewTargetNetFilterWithExclusions([]string{}, []string{}), + endpoints: []*endpoint.Endpoint{ + endpoint.NewEndpoint("foo", endpoint.RecordTypeA, "1.2.3.4"), + endpoint.NewEndpoint("foo", endpoint.RecordTypeA, "1.2.3.5"), + endpoint.NewEndpoint("foo", endpoint.RecordTypeA, "1.2.3.6"), + }, + expected: []*endpoint.Endpoint{ + endpoint.NewEndpoint("foo", endpoint.RecordTypeA, "1.2.3.4"), + endpoint.NewEndpoint("foo", endpoint.RecordTypeA, "1.2.3.5"), + endpoint.NewEndpoint("foo", endpoint.RecordTypeA, "1.2.3.6"), + }, + }, + { + title: "should include all targets when filters are not correctly set", + filters: endpoint.NewTargetNetFilterWithExclusions([]string{"8.8.8.8"}, []string{}), + endpoints: []*endpoint.Endpoint{ + endpoint.NewEndpoint("foo", endpoint.RecordTypeA, "10.0.0.1"), + endpoint.NewEndpoint("foo", endpoint.RecordTypeA, "8.8.8.8")}, + expected: []*endpoint.Endpoint{ + endpoint.NewEndpoint("foo", endpoint.RecordTypeA, "10.0.0.1"), + endpoint.NewEndpoint("foo", endpoint.RecordTypeA, "8.8.8.8"), + }, + }, + { + title: "should include internal when include filter is set", + filters: endpoint.NewTargetNetFilterWithExclusions([]string{"10.0.0.0/8"}, []string{}), + endpoints: []*endpoint.Endpoint{ + endpoint.NewEndpoint("foo", endpoint.RecordTypeA, "10.0.0.1"), + endpoint.NewEndpoint("foo", endpoint.RecordTypeA, "49.13.41.161")}, + expected: []*endpoint.Endpoint{ + endpoint.NewEndpoint("foo", endpoint.RecordTypeA, "10.0.0.1"), + }, + }, + { + title: "exclude internal keep public ips", + filters: endpoint.NewTargetNetFilterWithExclusions([]string{}, []string{"10.0.0.0/8"}), + endpoints: []*endpoint.Endpoint{ + endpoint.NewEndpoint("foo", endpoint.RecordTypeA, "10.0.178.43"), + endpoint.NewEndpoint("foo", endpoint.RecordTypeA, "10.0.1.101"), + endpoint.NewEndpoint("foo", endpoint.RecordTypeA, "49.13.41.161")}, + expected: []*endpoint.Endpoint{endpoint.NewEndpoint("foo", endpoint.RecordTypeA, "49.13.41.161")}, + }, + { + title: "should not exclude ipv6 when excluding ipv4", + filters: endpoint.NewTargetNetFilterWithExclusions([]string{}, []string{"10.0.0.0/8"}), + endpoints: []*endpoint.Endpoint{ + endpoint.NewEndpoint("foo", endpoint.RecordTypeA, "10.0.178.43"), + endpoint.NewEndpoint("foo", endpoint.RecordTypeAAAA, "2a01:asdf:asdf:asdf::1"), + }, + expected: []*endpoint.Endpoint{endpoint.NewEndpoint("foo", endpoint.RecordTypeAAAA, "2a01:asdf:asdf:asdf::1")}, + }, + { + title: "should not include ipv6 when including ipv4", + filters: endpoint.NewTargetNetFilterWithExclusions([]string{"10.0.0.0/8"}, []string{}), + endpoints: []*endpoint.Endpoint{ + endpoint.NewEndpoint("foo", endpoint.RecordTypeA, "10.0.178.43"), + endpoint.NewEndpoint("foo", endpoint.RecordTypeAAAA, "2a01:asdf:asdf:asdf::1"), + }, + expected: []*endpoint.Endpoint{endpoint.NewEndpoint("foo", endpoint.RecordTypeA, "10.0.178.43")}, + }, + } + for _, tt := range tests { + t.Run(tt.title, func(t *testing.T) { + echo := NewEchoSource(tt.endpoints) + src := NewTargetFilterSource(echo, tt.filters) + + endpoints, err := src.Endpoints(context.Background()) + require.NoError(t, err, "failed to get Endpoints") + + validateEndpoints(t, endpoints, tt.expected) + }) + } +} + +func TestTargetFilterSource_AddEventHandler(t *testing.T) { + tests := []struct { + title string + filters endpoint.TargetFilterInterface + times int + }{ + { + title: "should add event handler if target filter is enabled", + filters: endpoint.NewTargetNetFilterWithExclusions([]string{"10.0.0.0/8"}, []string{}), + times: 1, + }, + { + title: "should not add event handler if target filter is disabled", + filters: endpoint.NewTargetNetFilterWithExclusions([]string{}, []string{}), + times: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.title, func(t *testing.T) { + echo := NewEchoSource([]*endpoint.Endpoint{}) + + m := echo.(*echoSource) + m.On("AddEventHandler", t.Context()).Return() + + src := NewTargetFilterSource(echo, tt.filters) + src.AddEventHandler(t.Context(), func() {}) + + m.AssertNumberOfCalls(t, "AddEventHandler", tt.times) + }) + } +} From 1bfb970ace9762d809ebd892c012c1237b9bbb01 Mon Sep 17 00:00:00 2001 From: Ivan Ka <5395690+ivankatliarchuk@users.noreply.github.com> Date: Fri, 11 Jul 2025 18:15:28 +0100 Subject: [PATCH 33/49] fix(source/service): disable node informer when not required (#5613) * fix(source/service): disable node informer when service type filter is activated Signed-off-by: ivan katliarchuk fix(source/service): disable node informer when service type filter is activated Signed-off-by: ivan katliarchuk fix(source/service): disable node informer when service type filter is activated Signed-off-by: ivan katliarchuk fix(source/service): disable node informer when service type filter is activated Signed-off-by: ivan katliarchuk * fix(source/service): disable node informer when service type filter is activated Co-authored-by: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com> * fix(source/service): disable node informer when service type filter is activated Signed-off-by: ivan katliarchuk --------- Signed-off-by: ivan katliarchuk Co-authored-by: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com> --- source/informers/handlers.go | 38 ++++++++ source/informers/handlers_test.go | 50 +++++++++++ source/service.go | 67 +++++++++----- source/service_test.go | 141 +++++++++++++++++++++++++++++- 4 files changed, 269 insertions(+), 27 deletions(-) create mode 100644 source/informers/handlers.go create mode 100644 source/informers/handlers_test.go diff --git a/source/informers/handlers.go b/source/informers/handlers.go new file mode 100644 index 000000000..2d2067f45 --- /dev/null +++ b/source/informers/handlers.go @@ -0,0 +1,38 @@ +/* +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 informers + +import ( + log "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/client-go/tools/cache" +) + +func DefaultEventHandler(handlers ...func()) cache.ResourceEventHandler { + return cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj any) { + if u, ok := obj.(*unstructured.Unstructured); ok { + log.WithFields(log.Fields{ + "apiVersion": u.GetAPIVersion(), + "kind": u.GetKind(), + "namespace": u.GetNamespace(), + "name": u.GetName(), + }).Debug("added") + for _, handler := range handlers { + handler() + } + } + }, + } +} diff --git a/source/informers/handlers_test.go b/source/informers/handlers_test.go new file mode 100644 index 000000000..519b99b52 --- /dev/null +++ b/source/informers/handlers_test.go @@ -0,0 +1,50 @@ +/* +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 informers + +import ( + "testing" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func TestDefaultEventHandler_AddFunc(t *testing.T) { + tests := []struct { + name string + obj any + expected bool + }{ + { + name: "calls handler for unstructured object", + obj: &unstructured.Unstructured{}, + expected: true, + }, + { + name: "does not call handler for unknown object", + obj: "not-unstructured", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + called := false + handler := DefaultEventHandler(func() { called = true }) + handler.OnAdd(tt.obj, true) + if called != tt.expected { + t.Errorf("handler called = %v, want %v", called, tt.expected) + } + }) + } +} diff --git a/source/service.go b/source/service.go index 2606c0d8f..24f3b60ee 100644 --- a/source/service.go +++ b/source/service.go @@ -98,34 +98,39 @@ func NewServiceSource(ctx context.Context, kubeClient kubernetes.Interface, name serviceInformer := informerFactory.Core().V1().Services() endpointSlicesInformer := informerFactory.Discovery().V1().EndpointSlices() podInformer := informerFactory.Core().V1().Pods() - nodeInformer := informerFactory.Core().V1().Nodes() // Add default resource event handlers to properly initialize informer. - serviceInformer.Informer().AddEventHandler( + _, _ = serviceInformer.Informer().AddEventHandler( cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { }, }, ) - endpointSlicesInformer.Informer().AddEventHandler( + _, _ = endpointSlicesInformer.Informer().AddEventHandler( cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { }, }, ) - podInformer.Informer().AddEventHandler( - cache.ResourceEventHandlerFuncs{ - AddFunc: func(obj interface{}) { - }, - }, - ) - nodeInformer.Informer().AddEventHandler( + _, _ = podInformer.Informer().AddEventHandler( cache.ResourceEventHandlerFuncs{ AddFunc: func(obj interface{}) { }, }, ) + // Transform the slice into a map so it will be way much easier and fast to filter later + sTypesFilter, err := newServiceTypesFilter(serviceTypeFilter) + if err != nil { + return nil, err + } + + var nodeInformer coreinformers.NodeInformer + if sTypesFilter.isNodeInformerRequired() { + nodeInformer = informerFactory.Core().V1().Nodes() + _, _ = nodeInformer.Informer().AddEventHandler(informers.DefaultEventHandler()) + } + // Add an indexer to the EndpointSlice informer to index by the service name label err = endpointSlicesInformer.Informer().AddIndexers(cache.Indexers{ serviceNameIndexKey: func(obj any) ([]string, error) { @@ -153,12 +158,6 @@ func NewServiceSource(ctx context.Context, kubeClient kubernetes.Interface, name return nil, err } - // Transform the slice into a map so it will be way much easier and fast to filter later - sTypesFilter, err := newServiceTypesFilter(serviceTypeFilter) - if err != nil { - return nil, err - } - return &serviceSource{ client: kubeClient, namespace: namespace, @@ -197,7 +196,7 @@ func (sc *serviceSource) Endpoints(_ context.Context) ([]*endpoint.Endpoint, err return nil, err } - endpoints := []*endpoint.Endpoint{} + endpoints := make([]*endpoint.Endpoint, 0) for _, svc := range services { // Check controller annotation to see if we are responsible. @@ -364,6 +363,10 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri targets := annotations.TargetsFromTargetAnnotation(pod.Annotations) if len(targets) == 0 { if endpointsType == EndpointsTypeNodeExternalIP { + if sc.nodeInformer == nil { + log.Warnf("Skipping EndpointSlice %s/%s as --service-type-filter disable node informer", endpointSlice.Namespace, endpointSlice.Name) + continue + } node, err := sc.nodeInformer.Lister().Get(pod.Spec.NodeName) if err != nil { log.Errorf("Get node[%s] of pod[%s] error: %v; not adding any NodeExternalIP endpoints", pod.Spec.NodeName, pod.GetName(), err) @@ -459,7 +462,8 @@ func (sc *serviceSource) endpointsFromTemplate(svc *v1.Service) ([]*endpoint.End // endpointsFromService extracts the endpoints from a service object func (sc *serviceSource) endpoints(svc *v1.Service) []*endpoint.Endpoint { var endpoints []*endpoint.Endpoint - // Skip endpoints if we do not want entries from annotations + + // Skip endpoints if we do not want entries from annotations or service is excluded if sc.ignoreHostnameAnnotation { return endpoints } @@ -512,7 +516,7 @@ func (sc *serviceSource) filterByServiceType(services []*v1.Service) []*v1.Servi } var result []*v1.Service for _, service := range services { - if _, ok := sc.serviceTypeFilter.types[service.Spec.Type]; ok { + if sc.serviceTypeFilter.isProcessed(service.Spec.Type) { result = append(result, service) } } @@ -797,9 +801,12 @@ func (sc *serviceSource) AddEventHandler(_ context.Context, handler func()) { // Right now there is no way to remove event handler from informer, see: // https://github.com/kubernetes/kubernetes/issues/79610 - sc.serviceInformer.Informer().AddEventHandler(eventHandlerFunc(handler)) + _, _ = sc.serviceInformer.Informer().AddEventHandler(eventHandlerFunc(handler)) if sc.listenEndpointEvents { - sc.endpointSlicesInformer.Informer().AddEventHandler(eventHandlerFunc(handler)) + _, _ = sc.endpointSlicesInformer.Informer().AddEventHandler(eventHandlerFunc(handler)) + } + if sc.serviceTypeFilter.isNodeInformerRequired() { + _, _ = sc.nodeInformer.Informer().AddEventHandler(eventHandlerFunc(handler)) } } @@ -817,20 +824,32 @@ func newServiceTypesFilter(filter []string) (*serviceTypes, error) { enabled: false, }, nil } - types := make(map[v1.ServiceType]bool) + result := make(map[v1.ServiceType]bool) for _, serviceType := range filter { if _, ok := knownServiceTypes[v1.ServiceType(serviceType)]; !ok { return nil, fmt.Errorf("unsupported service type filter: %q. Supported types are: %q", serviceType, slices.Collect(maps.Keys(knownServiceTypes))) } - types[v1.ServiceType(serviceType)] = true + result[v1.ServiceType(serviceType)] = true } return &serviceTypes{ enabled: true, - types: types, + types: result, }, nil } +func (sc *serviceTypes) isProcessed(serviceType v1.ServiceType) bool { + return !sc.enabled || sc.types[serviceType] +} + +func (sc *serviceTypes) isNodeInformerRequired() bool { + if !sc.enabled { + return true + } + _, ok := sc.types[v1.ServiceTypeNodePort] + return ok +} + // conditionToBool converts an EndpointConditions condition to a bool value. func conditionToBool(v *bool) bool { if v == nil { diff --git a/source/service_test.go b/source/service_test.go index 406bdbc43..ecbe1fc83 100644 --- a/source/service_test.go +++ b/source/service_test.go @@ -302,6 +302,18 @@ func testServiceSourceEndpoints(t *testing.T) { {DNSName: "foo.fqdn.com", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, }, }, + { + title: "with excluded service type should not generate endpoints", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeLoadBalancer, + fqdnTemplate: "{{.Name}}.fqdn.org,{{.Name}}.fqdn.com", + labels: map[string]string{}, + annotations: map[string]string{}, + lbs: []string{"1.2.3.4"}, + serviceTypesFilter: []string{string(v1.ServiceTypeNodePort)}, + expected: []*endpoint.Endpoint{}, + }, { title: "FQDN template with multiple hostnames return an endpoint with target IP when ignoring annotations", svcNamespace: "testing", @@ -457,7 +469,7 @@ func testServiceSourceEndpoints(t *testing.T) { }, externalIPs: []string{}, lbs: []string{"1.2.3.4"}, - serviceTypesFilter: []string{}, + serviceTypesFilter: []string{string(v1.ServiceTypeLoadBalancer), string(v1.ServiceTypeNodePort)}, expected: []*endpoint.Endpoint{ {DNSName: "foo.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.2.3.4"}}, }, @@ -922,7 +934,6 @@ func testServiceSourceEndpoints(t *testing.T) { annotations: map[string]string{ hostnameAnnotationKey: "foo.example.org.", }, - externalIPs: []string{}, lbs: []string{"1.2.3.4"}, serviceTypesFilter: []string{string(v1.ServiceTypeLoadBalancer)}, expected: []*endpoint.Endpoint{}, @@ -4049,6 +4060,7 @@ func TestExternalServices(t *testing.T) { annotations map[string]string externalName string externalIPs []string + serviceTypeFilter []string expected []*endpoint.Endpoint expectError bool }{ @@ -4067,6 +4079,7 @@ func TestExternalServices(t *testing.T) { }, "111.111.111.111", []string{}, + []string{}, []*endpoint.Endpoint{ {DNSName: "service.example.org", Targets: endpoint.Targets{"111.111.111.111"}, RecordType: endpoint.RecordTypeA}, }, @@ -4087,6 +4100,7 @@ func TestExternalServices(t *testing.T) { }, "2001:db8::111", []string{}, + []string{}, []*endpoint.Endpoint{ {DNSName: "service.example.org", Targets: endpoint.Targets{"2001:db8::111"}, RecordType: endpoint.RecordTypeAAAA}, }, @@ -4107,6 +4121,7 @@ func TestExternalServices(t *testing.T) { }, "remote.example.com", []string{}, + []string{}, []*endpoint.Endpoint{ {DNSName: "service.example.org", Targets: endpoint.Targets{"remote.example.com"}, RecordType: endpoint.RecordTypeCNAME}, }, @@ -4127,6 +4142,7 @@ func TestExternalServices(t *testing.T) { }, "service.example.org", []string{"10.2.3.4", "11.2.3.4"}, + []string{}, []*endpoint.Endpoint{ {DNSName: "service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"10.2.3.4", "11.2.3.4"}}, }, @@ -4147,12 +4163,32 @@ func TestExternalServices(t *testing.T) { }, "service.example.org", []string{"10.2.3.4", "11.2.3.4", "2001:db8::1", "2001:db8::2"}, + []string{string(v1.ServiceTypeNodePort), string(v1.ServiceTypeExternalName)}, []*endpoint.Endpoint{ {DNSName: "service.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"10.2.3.4", "11.2.3.4"}}, {DNSName: "service.example.org", RecordType: endpoint.RecordTypeAAAA, Targets: endpoint.Targets{"2001:db8::1", "2001:db8::2"}}, }, false, }, + { + "annotated ExternalName service with externalIPs of dualstack and excluded in serviceTypeFilter", + "", + "testing", + "foo", + v1.ServiceTypeExternalName, + "", + "", + false, + map[string]string{"component": "foo"}, + map[string]string{ + hostnameAnnotationKey: "service.example.org", + }, + "service.example.org", + []string{"10.2.3.4", "11.2.3.4", "2001:db8::1", "2001:db8::2"}, + []string{string(v1.ServiceTypeNodePort), string(v1.ServiceTypeClusterIP)}, + []*endpoint.Endpoint{}, + false, + }, } { t.Run(tc.title, func(t *testing.T) { @@ -4190,7 +4226,7 @@ func TestExternalServices(t *testing.T) { true, false, false, - []string{}, + tc.serviceTypeFilter, tc.ignoreHostnameAnnotation, labels.Everything(), false, @@ -4267,6 +4303,73 @@ func BenchmarkServiceEndpoints(b *testing.B) { } } +func TestNewServiceSourceInformersEnabled(t *testing.T) { + tests := []struct { + name string + asserts func(svc *serviceSource) + svcFilter []string + }{ + { + name: "serviceTypeFilter is set to empty", + asserts: func(svc *serviceSource) { + assert.NotNil(t, svc) + assert.NotNil(t, svc.serviceTypeFilter) + assert.False(t, svc.serviceTypeFilter.enabled) + assert.NotNil(t, svc.nodeInformer) + }, + }, + { + name: "serviceTypeFilter contains NodePort", + svcFilter: []string{string(v1.ServiceTypeClusterIP)}, + asserts: func(svc *serviceSource) { + assert.NotNil(t, svc) + assert.NotNil(t, svc.serviceTypeFilter) + assert.True(t, svc.serviceTypeFilter.enabled) + assert.Nil(t, svc.nodeInformer) + }, + }, + { + name: "serviceTypeFilter contains NodePort", + svcFilter: []string{string(v1.ServiceTypeNodePort)}, + asserts: func(svc *serviceSource) { + assert.NotNil(t, svc) + assert.NotNil(t, svc.serviceTypeFilter) + assert.True(t, svc.serviceTypeFilter.enabled) + assert.NotNil(t, svc.nodeInformer) + }, + }, + } + + for _, ts := range tests { + t.Run(ts.name, func(t *testing.T) { + svc, err := NewServiceSource( + t.Context(), + fake.NewClientset(), + "default", + "", + "", + false, + "", + true, + false, + false, + ts.svcFilter, + false, + labels.Everything(), + false, + false, + false, + ) + require.NoError(t, err) + svcSrc, ok := svc.(*serviceSource) + if !ok { + require.Fail(t, "expected serviceSource") + } + ts.asserts(svcSrc) + }) + } +} + func TestNewServiceSourceWithServiceTypeFilters_Unsupported(t *testing.T) { serviceTypeFilter := []string{"ClusterIP", "ServiceTypeNotExist"} @@ -4520,3 +4623,35 @@ func createTestServicesByType(namespace string, typeCounts map[v1.ServiceType]in }) return services } + +func TestServiceTypes_isNodeInformerRequired(t *testing.T) { + tests := []struct { + name string + filter []string + want bool + }{ + { + name: "NodePort type present", + filter: []string{string(v1.ServiceTypeNodePort)}, + want: true, + }, + { + name: "NodePort type absent, filter enabled", + filter: []string{string(v1.ServiceTypeLoadBalancer)}, + want: false, + }, + { + name: "NodePort and other filters present", + filter: []string{string(v1.ServiceTypeLoadBalancer), string(v1.ServiceTypeNodePort)}, + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + filter, _ := newServiceTypesFilter(tt.filter) + got := filter.isNodeInformerRequired() + assert.Equal(t, tt.want, got) + }) + } +} From 18c47f0ac35d9d96f1b4e1b20f94d7a75828984b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 12 Jul 2025 02:47:28 -0700 Subject: [PATCH 34/49] chore(deps): bump the dev-dependencies group across 1 directory with 8 updates (#5637) Bumps the dev-dependencies group with 6 updates in the / directory: | Package | From | To | | --- | --- | --- | | [github.com/Azure/azure-sdk-for-go/sdk/azcore](https://github.com/Azure/azure-sdk-for-go) | `1.18.0` | `1.18.1` | | [github.com/miekg/dns](https://github.com/miekg/dns) | `1.1.66` | `1.1.67` | | [go.etcd.io/etcd/api/v3](https://github.com/etcd-io/etcd) | `3.6.1` | `3.6.2` | | [go.etcd.io/etcd/client/v3](https://github.com/etcd-io/etcd) | `3.6.1` | `3.6.2` | | [golang.org/x/net](https://github.com/golang/net) | `0.41.0` | `0.42.0` | | [google.golang.org/api](https://github.com/googleapis/google-api-go-client) | `0.240.0` | `0.241.0` | Updates `github.com/Azure/azure-sdk-for-go/sdk/azcore` from 1.18.0 to 1.18.1 - [Release notes](https://github.com/Azure/azure-sdk-for-go/releases) - [Changelog](https://github.com/Azure/azure-sdk-for-go/blob/main/documentation/go-mgmt-sdk-release-guideline.md) - [Commits](https://github.com/Azure/azure-sdk-for-go/compare/sdk/azcore/v1.18.0...sdk/azcore/v1.18.1) Updates `github.com/miekg/dns` from 1.1.66 to 1.1.67 - [Changelog](https://github.com/miekg/dns/blob/master/Makefile.release) - [Commits](https://github.com/miekg/dns/compare/v1.1.66...v1.1.67) Updates `go.etcd.io/etcd/api/v3` from 3.6.1 to 3.6.2 - [Release notes](https://github.com/etcd-io/etcd/releases) - [Commits](https://github.com/etcd-io/etcd/compare/v3.6.1...v3.6.2) Updates `go.etcd.io/etcd/client/v3` from 3.6.1 to 3.6.2 - [Release notes](https://github.com/etcd-io/etcd/releases) - [Commits](https://github.com/etcd-io/etcd/compare/v3.6.1...v3.6.2) Updates `golang.org/x/net` from 0.41.0 to 0.42.0 - [Commits](https://github.com/golang/net/compare/v0.41.0...v0.42.0) Updates `golang.org/x/sync` from 0.15.0 to 0.16.0 - [Commits](https://github.com/golang/sync/compare/v0.15.0...v0.16.0) Updates `golang.org/x/text` from 0.26.0 to 0.27.0 - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.26.0...v0.27.0) Updates `google.golang.org/api` from 0.240.0 to 0.241.0 - [Release notes](https://github.com/googleapis/google-api-go-client/releases) - [Changelog](https://github.com/googleapis/google-api-go-client/blob/main/CHANGES.md) - [Commits](https://github.com/googleapis/google-api-go-client/compare/v0.240.0...v0.241.0) --- updated-dependencies: - dependency-name: github.com/Azure/azure-sdk-for-go/sdk/azcore dependency-version: 1.18.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: github.com/miekg/dns dependency-version: 1.1.67 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: go.etcd.io/etcd/api/v3 dependency-version: 3.6.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: go.etcd.io/etcd/client/v3 dependency-version: 3.6.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: golang.org/x/net dependency-version: 0.42.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: golang.org/x/sync dependency-version: 0.16.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: golang.org/x/text dependency-version: 0.27.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: google.golang.org/api dependency-version: 0.241.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dev-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 26 +++++++++++++------------- go.sum | 52 ++++++++++++++++++++++++++-------------------------- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/go.mod b/go.mod index a56ae2817..0b67f521c 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.24.2 require ( cloud.google.com/go/compute/metadata v0.7.0 - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 @@ -41,7 +41,7 @@ require ( github.com/linki/instrumented_http v0.3.0 github.com/linode/linodego v1.53.0 github.com/maxatome/go-testdeep v1.14.0 - github.com/miekg/dns v1.1.66 + github.com/miekg/dns v1.1.67 github.com/openshift/api v0.0.0-20230607130528-611114dca681 github.com/openshift/client-go v0.0.0-20230607134213-3cd0021bbee3 github.com/oracle/oci-go-sdk/v65 v65.95.1 @@ -55,15 +55,15 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.10.0 github.com/transip/gotransip/v6 v6.26.0 - go.etcd.io/etcd/api/v3 v3.6.1 - go.etcd.io/etcd/client/v3 v3.6.1 + go.etcd.io/etcd/api/v3 v3.6.2 + go.etcd.io/etcd/client/v3 v3.6.2 go.uber.org/ratelimit v0.3.1 - golang.org/x/net v0.41.0 + golang.org/x/net v0.42.0 golang.org/x/oauth2 v0.30.0 - golang.org/x/sync v0.15.0 - golang.org/x/text v0.26.0 + golang.org/x/sync v0.16.0 + golang.org/x/text v0.27.0 golang.org/x/time v0.12.0 - google.golang.org/api v0.240.0 + google.golang.org/api v0.241.0 gopkg.in/ns1/ns1-go.v2 v2.14.4 istio.io/api v1.26.2 istio.io/client-go v1.26.2 @@ -171,7 +171,7 @@ require ( github.com/x448/float16 v0.8.4 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect - go.etcd.io/etcd/client/pkg/v3 v3.6.1 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.6.2 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect go.opentelemetry.io/otel v1.36.0 // indirect @@ -180,11 +180,11 @@ require ( go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/crypto v0.39.0 // indirect + golang.org/x/crypto v0.40.0 // indirect golang.org/x/mod v0.25.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/term v0.32.0 // indirect - golang.org/x/tools v0.33.0 // indirect + golang.org/x/sys v0.34.0 // indirect + golang.org/x/term v0.33.0 // indirect + golang.org/x/tools v0.34.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/grpc v1.73.0 // indirect diff --git a/go.sum b/go.sum index 0dcef98ed..3983bc86c 100644 --- a/go.sum +++ b/go.sum @@ -16,8 +16,8 @@ github.com/99designs/gqlgen v0.17.73 h1:A3Ki+rHWqKbAOlg5fxiZBnz6OjW3nwupDHEG15gE github.com/99designs/gqlgen v0.17.73/go.mod h1:2RyGWjy2k7W9jxrs8MOQthXGkD3L3oGr0jXW3Pu8lGg= github.com/Azure/azure-sdk-for-go v16.2.1+incompatible h1:KnPIugL51v3N3WwvaSmZbxukD1WuWXOiE9fRdu32f2I= github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1 h1:Wc1ml6QlJs2BHQ/9Bqu1jiyggbsSjramq2oUmp5WeIo= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= @@ -725,8 +725,8 @@ github.com/mholt/archiver/v3 v3.3.0/go.mod h1:YnQtqsp+94Rwd0D/rk5cnLrxusUBUXg+08 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.6/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= -github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE= -github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE= +github.com/miekg/dns v1.1.67 h1:kg0EHj0G4bfT5/oOys6HhZw4vmMlnoZ+gDu8tJ/AlI0= +github.com/miekg/dns v1.1.67/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= @@ -1052,12 +1052,12 @@ github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wK go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= -go.etcd.io/etcd/api/v3 v3.6.1 h1:yJ9WlDih9HT457QPuHt/TH/XtsdN2tubyxyQHSHPsEo= -go.etcd.io/etcd/api/v3 v3.6.1/go.mod h1:lnfuqoGsXMlZdTJlact3IB56o3bWp1DIlXPIGKRArto= -go.etcd.io/etcd/client/pkg/v3 v3.6.1 h1:CxDVv8ggphmamrXM4Of8aCC8QHzDM4tGcVr9p2BSoGk= -go.etcd.io/etcd/client/pkg/v3 v3.6.1/go.mod h1:aTkCp+6ixcVTZmrJGa7/Mc5nMNs59PEgBbq+HCmWyMc= -go.etcd.io/etcd/client/v3 v3.6.1 h1:KelkcizJGsskUXlsxjVrSmINvMMga0VWwFF0tSPGEP0= -go.etcd.io/etcd/client/v3 v3.6.1/go.mod h1:fCbPUdjWNLfx1A6ATo9syUmFVxqHH9bCnPLBZmnLmMY= +go.etcd.io/etcd/api/v3 v3.6.2 h1:25aCkIMjUmiiOtnBIp6PhNj4KdcURuBak0hU2P1fgRc= +go.etcd.io/etcd/api/v3 v3.6.2/go.mod h1:eFhhvfR8Px1P6SEuLT600v+vrhdDTdcfMzmnxVXXSbk= +go.etcd.io/etcd/client/pkg/v3 v3.6.2 h1:zw+HRghi/G8fKpgKdOcEKpnBTE4OO39T6MegA0RopVU= +go.etcd.io/etcd/client/pkg/v3 v3.6.2/go.mod h1:sbdzr2cl3HzVmxNw//PH7aLGVtY4QySjQFuaCgcRFAI= +go.etcd.io/etcd/client/v3 v3.6.2 h1:RgmcLJxkpHqpFvgKNwAQHX3K+wsSARMXKgjmUSpoSKQ= +go.etcd.io/etcd/client/v3 v3.6.2/go.mod h1:PL7e5QMKzjybn0FosgiWvCUDzvdChpo5UgGR4Sk4Gzc= go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= @@ -1127,8 +1127,8 @@ golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= -golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= -golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= +golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1196,8 +1196,8 @@ golang.org/x/net v0.0.0-20220725212005-46097bf591d3/go.mod h1:AaygXjzTFtRAg2ttMY golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190130055435-99b60b757ec1/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1215,8 +1215,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= -golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180425194835-bb9c189858d9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1273,8 +1273,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1282,8 +1282,8 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= -golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= -golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= +golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1294,8 +1294,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1338,8 +1338,8 @@ golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= -golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1352,8 +1352,8 @@ gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZ google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.240.0 h1:PxG3AA2UIqT1ofIzWV2COM3j3JagKTKSwy7L6RHNXNU= -google.golang.org/api v0.240.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= +google.golang.org/api v0.241.0 h1:QKwqWQlkc6O895LchPEDUSYr22Xp3NCxpQRiWTB6avE= +google.golang.org/api v0.241.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= From 179bbb56b7a9834eaa94ef5a866562f27fa7c1e3 Mon Sep 17 00:00:00 2001 From: Ivan Ka <5395690+ivankatliarchuk@users.noreply.github.com> Date: Sat, 12 Jul 2025 18:41:29 +0100 Subject: [PATCH 35/49] fix(provider/aws): null pointer when records mailformed (#5639) * fix(provider/aws): null pointer when records mailformed Signed-off-by: ivan katliarchuk * fix(provider/aws): null pointer when records mailformed Co-authored-by: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com> * fix(provider/aws): null pointer when records mailformed Signed-off-by: ivan katliarchuk --------- Signed-off-by: ivan katliarchuk Co-authored-by: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com> --- provider/aws/aws.go | 4 ++++ provider/aws/aws_test.go | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/provider/aws/aws.go b/provider/aws/aws.go index a5796ae1a..1ddb35cce 100644 --- a/provider/aws/aws.go +++ b/provider/aws/aws.go @@ -641,6 +641,10 @@ func (p *AWSProvider) createUpdateChanges(newEndpoints, oldEndpoints []*endpoint var updates []*endpoint.Endpoint for i, newE := range newEndpoints { + if i >= len(oldEndpoints) || oldEndpoints[i] == nil { + log.Debugf("skip %s as endpoint not found in current endpoints", newE.DNSName) + continue + } oldE := oldEndpoints[i] if p.requiresDeleteCreate(oldE, newE) { deletes = append(deletes, oldE) diff --git a/provider/aws/aws_test.go b/provider/aws/aws_test.go index 474c97250..a269c3ede 100644 --- a/provider/aws/aws_test.go +++ b/provider/aws/aws_test.go @@ -2817,3 +2817,36 @@ func TestGeoProximityWithBias(t *testing.T) { }) } } + +func TestAWSProvider_createUpdateChanges_NewMoreThanOld(t *testing.T) { + provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"foo.bar."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), true, false, nil) + + oldEndpoints := []*endpoint.Endpoint{ + endpoint.NewEndpointWithTTL("record1.foo.bar.", endpoint.RecordTypeA, endpoint.TTL(300), "1.1.1.1"), + nil, + } + newEndpoints := []*endpoint.Endpoint{ + endpoint.NewEndpointWithTTL("record1.foo.bar.", endpoint.RecordTypeA, endpoint.TTL(300), "1.1.1.1"), + endpoint.NewEndpointWithTTL("record2.foo.bar.", endpoint.RecordTypeA, endpoint.TTL(300), "2.2.2.2"), + endpoint.NewEndpointWithTTL("record3.foo.bar.", endpoint.RecordTypeA, endpoint.TTL(300), "3.3.3.3"), + } + + changes := provider.createUpdateChanges(newEndpoints, oldEndpoints) + + // record2 should be created, record1 should be upserted + var creates, upserts, deletes int + for _, c := range changes { + switch c.Action { + case route53types.ChangeActionCreate: + creates++ + case route53types.ChangeActionUpsert: + upserts++ + case route53types.ChangeActionDelete: + deletes++ + } + } + + require.Equal(t, 0, creates, "should create the extra new endpoint") + require.Equal(t, 1, upserts, "should upsert the matching endpoint") + require.Equal(t, 0, deletes, "should not delete anything") +} From 8c9af76c2884e44972eaad403f91e922e65b3c87 Mon Sep 17 00:00:00 2001 From: Raffaele Di Fazio Date: Sun, 13 Jul 2025 07:38:22 +0200 Subject: [PATCH 36/49] docs: add information on external webhook usage (#5606) * add information on external webhook usage * Update README.md Co-authored-by: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com> * Update README.md Co-authored-by: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com> --------- Co-authored-by: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com> --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b66de266b..45ad8e06f 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,11 @@ No new provider will be added to ExternalDNS _in-tree_. ExternalDNS has introduced a webhook system, which can be used to add a new provider. See PR #3063 for all the discussions about it. -Known providers using webhooks: +Some known providers using webhooks are the ones in the table below. + +**NOTE**: The maintainers of ExternalDNS have not reviewed those providers, use them at your own risk and following the license +and usage recommendations provided by the respective projects. The maintainers of ExternalDNS take no responsibility for any issue or damage +from the usage of any externally developed webhook. | Provider | Repo | | --------------------- | -------------------------------------------------------------------- | From a386197f0a31dbdf8b099ec6a54731ddc5d91359 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Jul 2025 00:18:23 -0700 Subject: [PATCH 37/49] chore(deps): bump github.com/cloudflare/cloudflare-go/v4 (#5645) Bumps the dev-dependencies group with 1 update: [github.com/cloudflare/cloudflare-go/v4](https://github.com/cloudflare/cloudflare-go). Updates `github.com/cloudflare/cloudflare-go/v4` from 4.5.1 to 4.6.0 - [Release notes](https://github.com/cloudflare/cloudflare-go/releases) - [Changelog](https://github.com/cloudflare/cloudflare-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/cloudflare/cloudflare-go/compare/v4.5.1...v4.6.0) --- updated-dependencies: - dependency-name: github.com/cloudflare/cloudflare-go/v4 dependency-version: 4.6.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dev-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 0b67f521c..f2f935756 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( github.com/cenkalti/backoff/v5 v5.0.2 github.com/civo/civogo v0.6.1 github.com/cloudflare/cloudflare-go v0.115.0 - github.com/cloudflare/cloudflare-go/v4 v4.5.1 + github.com/cloudflare/cloudflare-go/v4 v4.6.0 github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381 github.com/datawire/ambassador v1.12.4 github.com/denverdino/aliyungo v0.0.0-20230411124812-ab98a9173ace diff --git a/go.sum b/go.sum index 3983bc86c..702b715a1 100644 --- a/go.sum +++ b/go.sum @@ -186,8 +186,8 @@ github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/cloudflare-go v0.115.0 h1:84/dxeeXweCc0PN5Cto44iTA8AkG1fyT11yPO5ZB7sM= github.com/cloudflare/cloudflare-go v0.115.0/go.mod h1:Ds6urDwn/TF2uIU24mu7H91xkKP8gSAHxQ44DSZgVmU= -github.com/cloudflare/cloudflare-go/v4 v4.5.1 h1:ZQgQ7QO+M9rK0KYx1CmppuG15ZTYGHn8F9/Fh7mCuQQ= -github.com/cloudflare/cloudflare-go/v4 v4.5.1/go.mod h1:XcYpLe7Mf6FN87kXzEWVnJ6z+vskW/k6eUqgqfhFE9k= +github.com/cloudflare/cloudflare-go/v4 v4.6.0 h1:ZaWwXjHFR5NoY8UEf4QFY0g3KTi72kqqEXpajV610/o= +github.com/cloudflare/cloudflare-go/v4 v4.6.0/go.mod h1:XcYpLe7Mf6FN87kXzEWVnJ6z+vskW/k6eUqgqfhFE9k= github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381 h1:rdRS5BT13Iae9ssvcslol66gfOOXjaLYwqerEn/cl9s= github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381/go.mod h1:e5+USP2j8Le2M0Jo3qKPFnNhuo1wueU4nWHCXBOfQ14= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= From a270a32bf69945786da4181ee7be9a903977a36a Mon Sep 17 00:00:00 2001 From: Kai Udo <76635578+u-kai@users.noreply.github.com> Date: Mon, 14 Jul 2025 18:18:24 +0900 Subject: [PATCH 38/49] fix(helm): resolve RBAC permissions for namespaced gateway sources (#5578) * fix(helm): resolve RBAC permissions for namespaced gateway sources * feat(helm): add support for gateway namespace in RBAC configuration * chore(helm): update docs and fix formatting issues * fix(helm): revert README changes and add gatewayNamespace docs * chore lint fmt --- charts/external-dns/README.md | 1 + charts/external-dns/templates/_helpers.tpl | 9 + .../external-dns/templates/clusterrole.yaml | 33 ++- .../templates/clusterrolebinding.yaml | 35 +++ charts/external-dns/templates/deployment.yaml | 3 + charts/external-dns/tests/rbac_test.yaml | 235 ++++++++++++++++++ charts/external-dns/values.yaml | 3 + 7 files changed, 318 insertions(+), 1 deletion(-) diff --git a/charts/external-dns/README.md b/charts/external-dns/README.md index 24f7ed9b3..2daf4f005 100644 --- a/charts/external-dns/README.md +++ b/charts/external-dns/README.md @@ -109,6 +109,7 @@ If `namespaced` is set to `true`, please ensure that `sources` my only contains | extraVolumeMounts | list | `[]` | Extra [volume mounts](https://kubernetes.io/docs/concepts/storage/volumes/) for the `external-dns` container. | | extraVolumes | list | `[]` | Extra [volumes](https://kubernetes.io/docs/concepts/storage/volumes/) for the `Pod`. | | fullnameOverride | string | `nil` | Override the full name of the chart. | +| gatewayNamespace | string | `nil` | _Gateway API_ gateway namespace to watch. | | global.imagePullSecrets | list | `[]` | Global image pull secrets. | | image.pullPolicy | string | `"IfNotPresent"` | Image pull policy for the `external-dns` container. | | image.repository | string | `"registry.k8s.io/external-dns/external-dns"` | Image repository for the `external-dns` container. | diff --git a/charts/external-dns/templates/_helpers.tpl b/charts/external-dns/templates/_helpers.tpl index a7277c8ef..aad09822e 100644 --- a/charts/external-dns/templates/_helpers.tpl +++ b/charts/external-dns/templates/_helpers.tpl @@ -103,3 +103,12 @@ labelSelector: matchLabels: {{ include "external-dns.selectorLabels" . | nindent 4 }} {{- end }} + +{{/* +Check if any Gateway API sources are enabled +*/}} +{{- define "external-dns.hasGatewaySources" -}} +{{- if or (has "gateway-httproute" .Values.sources) (has "gateway-grpcroute" .Values.sources) (has "gateway-tlsroute" .Values.sources) (has "gateway-tcproute" .Values.sources) (has "gateway-udproute" .Values.sources) -}} +true +{{- end -}} +{{- end }} diff --git a/charts/external-dns/templates/clusterrole.yaml b/charts/external-dns/templates/clusterrole.yaml index a3d8cb777..f418416e2 100644 --- a/charts/external-dns/templates/clusterrole.yaml +++ b/charts/external-dns/templates/clusterrole.yaml @@ -58,14 +58,18 @@ rules: resources: ["dnsendpoints/status"] verbs: ["*"] {{- end }} -{{- if or (has "gateway-httproute" .Values.sources) (has "gateway-grpcroute" .Values.sources) (has "gateway-tlsroute" .Values.sources) (has "gateway-tcproute" .Values.sources) (has "gateway-udproute" .Values.sources) }} +{{- if include "external-dns.hasGatewaySources" . }} +{{- if or (not .Values.namespaced) (and .Values.namespaced (not .Values.gatewayNamespace)) }} - apiGroups: ["gateway.networking.k8s.io"] resources: ["gateways"] verbs: ["get","watch","list"] +{{- end }} +{{- if not .Values.namespaced }} - apiGroups: [""] resources: ["namespaces"] verbs: ["get","watch","list"] {{- end }} +{{- end }} {{- if has "gateway-httproute" .Values.sources }} - apiGroups: ["gateway.networking.k8s.io"] resources: ["httproutes"] @@ -127,4 +131,31 @@ rules: {{- with .Values.rbac.additionalPermissions }} {{- toYaml . | nindent 2 }} {{- end }} +{{- if and .Values.rbac.create .Values.namespaced (include "external-dns.hasGatewaySources" .) }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ template "external-dns.fullname" . }}-namespaces + labels: + {{- include "external-dns.labels" . | nindent 4 }} +rules: + - apiGroups: [""] + resources: ["namespaces"] + verbs: ["get","watch","list"] +{{- if .Values.gatewayNamespace }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ template "external-dns.fullname" . }}-gateway + namespace: {{ .Values.gatewayNamespace }} + labels: + {{- include "external-dns.labels" . | nindent 4 }} +rules: + - apiGroups: ["gateway.networking.k8s.io"] + resources: ["gateways"] + verbs: ["get","watch","list"] +{{- end }} +{{- end }} {{- end }} diff --git a/charts/external-dns/templates/clusterrolebinding.yaml b/charts/external-dns/templates/clusterrolebinding.yaml index 74a51476f..49400c0be 100644 --- a/charts/external-dns/templates/clusterrolebinding.yaml +++ b/charts/external-dns/templates/clusterrolebinding.yaml @@ -13,4 +13,39 @@ subjects: - kind: ServiceAccount name: {{ template "external-dns.serviceAccountName" . }} namespace: {{ .Release.Namespace }} +{{- if and .Values.rbac.create .Values.namespaced (include "external-dns.hasGatewaySources" .) }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ template "external-dns.fullname" . }}-namespaces + labels: + {{- include "external-dns.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ template "external-dns.fullname" . }}-namespaces +subjects: + - kind: ServiceAccount + name: {{ template "external-dns.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +{{- if .Values.gatewayNamespace }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ template "external-dns.fullname" . }}-gateway + namespace: {{ .Values.gatewayNamespace }} + labels: + {{- include "external-dns.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ template "external-dns.fullname" . }}-gateway +subjects: + - kind: ServiceAccount + name: {{ template "external-dns.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +{{- end }} +{{- end }} {{- end }} diff --git a/charts/external-dns/templates/deployment.yaml b/charts/external-dns/templates/deployment.yaml index f0c967a33..7db118370 100644 --- a/charts/external-dns/templates/deployment.yaml +++ b/charts/external-dns/templates/deployment.yaml @@ -111,6 +111,9 @@ spec: {{- if .Values.namespaced }} - --namespace={{ .Release.Namespace }} {{- end }} + {{- if .Values.gatewayNamespace }} + - --gateway-namespace={{ .Values.gatewayNamespace }} + {{- end }} {{- range .Values.domainFilters }} - --domain-filter={{ . }} {{- end }} diff --git a/charts/external-dns/tests/rbac_test.yaml b/charts/external-dns/tests/rbac_test.yaml index 9e061c649..c8fcaea2c 100644 --- a/charts/external-dns/tests/rbac_test.yaml +++ b/charts/external-dns/tests/rbac_test.yaml @@ -156,3 +156,238 @@ tests: - apiGroups: ["gateway.networking.k8s.io"] resources: ["udproutes"] verbs: ["get","watch","list"] + + - it: should create Role instead of ClusterRole when namespaced is true + set: + namespaced: true + sources: + - service + asserts: + - isKind: + of: Role + template: clusterrole.yaml + - equal: + path: metadata.name + value: rbac-external-dns + template: clusterrole.yaml + + - it: should create RoleBinding instead of ClusterRoleBinding when namespaced is true + set: + namespaced: true + sources: + - service + asserts: + - isKind: + of: RoleBinding + template: clusterrolebinding.yaml + - equal: + path: metadata.name + value: rbac-external-dns-viewer + template: clusterrolebinding.yaml + + - it: should create all required resources when namespaced=true and gatewayNamespace is specified + set: + namespaced: true + gatewayNamespace: gateway-ns + sources: + - gateway-httproute + asserts: + # Should have: main Role + ClusterRole for namespaces + Gateway Role + - hasDocuments: + count: 3 + template: clusterrole.yaml + - hasDocuments: + count: 3 + template: clusterrolebinding.yaml + + # Main role should exist and contain route permissions but NOT gateway permissions + - isKind: + of: Role + documentSelector: + path: metadata.name + value: rbac-external-dns + template: clusterrole.yaml + - contains: + path: rules + content: + apiGroups: ["gateway.networking.k8s.io"] + resources: ["httproutes"] + verbs: ["get","watch","list"] + documentSelector: + path: metadata.name + value: rbac-external-dns + template: clusterrole.yaml + - notContains: + path: rules + content: + apiGroups: ["gateway.networking.k8s.io"] + resources: ["gateways"] + verbs: ["get","watch","list"] + documentSelector: + path: metadata.name + value: rbac-external-dns + template: clusterrole.yaml + + # ClusterRole for namespaces should exist + - isKind: + of: ClusterRole + documentSelector: + path: metadata.name + value: rbac-external-dns-namespaces + template: clusterrole.yaml + + # Gateway role should exist and have gateway permissions only + - isKind: + of: Role + documentSelector: + path: metadata.name + value: rbac-external-dns-gateway + template: clusterrole.yaml + - equal: + path: metadata.namespace + value: gateway-ns + documentSelector: + path: metadata.name + value: rbac-external-dns-gateway + template: clusterrole.yaml + - contains: + path: rules + content: + apiGroups: ["gateway.networking.k8s.io"] + resources: ["gateways"] + verbs: ["get","watch","list"] + documentSelector: + path: metadata.name + value: rbac-external-dns-gateway + template: clusterrole.yaml + + + - it: should create main Role with gateway permissions and ClusterRole for namespaces when namespaced=true and gatewayNamespace is not set + set: + namespaced: true + sources: + - gateway-httproute + asserts: + # Should have: main Role + ClusterRole for namespaces + - hasDocuments: + count: 2 + template: clusterrole.yaml + - hasDocuments: + count: 2 + template: clusterrolebinding.yaml + # Main Role should exist and contain gateway permissions + - isKind: + of: Role + documentSelector: + path: metadata.name + value: rbac-external-dns + template: clusterrole.yaml + - contains: + path: rules + content: + apiGroups: ["gateway.networking.k8s.io"] + resources: ["gateways"] + verbs: ["get","watch","list"] + documentSelector: + path: metadata.name + value: rbac-external-dns + template: clusterrole.yaml + - contains: + path: rules + content: + apiGroups: ["gateway.networking.k8s.io"] + resources: ["httproutes"] + verbs: ["get","watch","list"] + documentSelector: + path: metadata.name + value: rbac-external-dns + template: clusterrole.yaml + # ClusterRole for namespaces should exist + - isKind: + of: ClusterRole + documentSelector: + path: metadata.name + value: rbac-external-dns-namespaces + template: clusterrole.yaml + - contains: + path: rules + content: + apiGroups: [""] + resources: ["namespaces"] + verbs: ["get","watch","list"] + documentSelector: + path: metadata.name + value: rbac-external-dns-namespaces + template: clusterrole.yaml + + - it: should create ClusterRole with all permissions when namespaced=false and gateway sources exist + set: + namespaced: false + sources: + - gateway-httproute + asserts: + - hasDocuments: + count: 1 + template: clusterrole.yaml + - hasDocuments: + count: 1 + template: clusterrolebinding.yaml + - isKind: + of: ClusterRole + template: clusterrole.yaml + - contains: + path: rules + content: + apiGroups: ["gateway.networking.k8s.io"] + resources: ["gateways"] + verbs: ["get","watch","list"] + template: clusterrole.yaml + - contains: + path: rules + content: + apiGroups: [""] + resources: ["namespaces"] + verbs: ["get","watch","list"] + template: clusterrole.yaml + + - it: should create only ClusterRole when namespaced=false and no gateway sources + set: + namespaced: false + sources: + - service + - ingress + asserts: + - hasDocuments: + count: 1 + template: clusterrole.yaml + - hasDocuments: + count: 1 + template: clusterrolebinding.yaml + - isKind: + of: ClusterRole + template: clusterrole.yaml + - notContains: + path: rules + content: + apiGroups: ["gateway.networking.k8s.io"] + template: clusterrole.yaml + + - it: should create only Role when namespaced=true and no gateway sources + set: + namespaced: true + sources: + - service + - ingress + asserts: + - hasDocuments: + count: 1 + template: clusterrole.yaml + - hasDocuments: + count: 1 + template: clusterrolebinding.yaml + - isKind: + of: Role + template: clusterrole.yaml + - isKind: + of: RoleBinding + template: clusterrolebinding.yaml diff --git a/charts/external-dns/values.yaml b/charts/external-dns/values.yaml index a6fa3212b..8bc037974 100644 --- a/charts/external-dns/values.yaml +++ b/charts/external-dns/values.yaml @@ -205,6 +205,9 @@ triggerLoopOnEvent: false # -- if `true`, _ExternalDNS_ will run in a namespaced scope (`Role`` and `Rolebinding`` will be namespaced too). namespaced: false +# -- _Gateway API_ gateway namespace to watch. +gatewayNamespace: # @schema type:[string, null]; default: null + # -- _Kubernetes_ resources to monitor for DNS entries. sources: - service From 73b8fb0da7850d8e0be34b8401d4b200e475b657 Mon Sep 17 00:00:00 2001 From: Colton Hughes <38597291+coltonhughes@users.noreply.github.com> Date: Mon, 14 Jul 2025 04:18:31 -0500 Subject: [PATCH 39/49] fix(helm): Update helm value schema to allow `create-only` policy type (#5627) * fix(helm): Update schema for helm to allow `create-only` * fix(docs): Update changelog to reflect addition of `create-only` policy. * chore(docs): Update changelog with PR * fix(helm): Undo improper spacing on comments * chore(docs): Update README.md with new option * fix(helm): Add EOF newline since format-on-save removed it --- charts/external-dns/CHANGELOG.md | 4 ++++ charts/external-dns/README.md | 2 +- charts/external-dns/values.schema.json | 3 ++- charts/external-dns/values.yaml | 4 ++-- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/charts/external-dns/CHANGELOG.md b/charts/external-dns/CHANGELOG.md index 16a129a9a..e30a440fa 100644 --- a/charts/external-dns/CHANGELOG.md +++ b/charts/external-dns/CHANGELOG.md @@ -18,6 +18,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [UNRELEASED] +### Fixed + +- Fixed the lack of schema support for `create-only` dns policy in helm values ([#5627](https://github.com/kubernetes-sigs/external-dns/pull/5627)) _@coltonhughes_ + ### Changed - Update RBAC for `Service` source to support `EndpointSlices`. ([#5493](https://github.com/kubernetes-sigs/external-dns/pull/5493)) _@vflaux_ diff --git a/charts/external-dns/README.md b/charts/external-dns/README.md index 2daf4f005..f67502aec 100644 --- a/charts/external-dns/README.md +++ b/charts/external-dns/README.md @@ -128,7 +128,7 @@ If `namespaced` is set to `true`, please ensure that `sources` my only contains | podAnnotations | object | `{}` | Annotations to add to the `Pod`. | | podLabels | object | `{}` | Labels to add to the `Pod`. | | podSecurityContext | object | See _values.yaml_ | [Pod security context](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#podsecuritycontext-v1-core), this supports full customisation. | -| policy | string | `"upsert-only"` | How DNS records are synchronized between sources and providers; available values are `sync` & `upsert-only`. | +| policy | string | `"upsert-only"` | How DNS records are synchronized between sources and providers; available values are `create-only`, `sync`, & `upsert-only`. | | priorityClassName | string | `nil` | Priority class name for the `Pod`. | | provider.name | string | `"aws"` | _ExternalDNS_ provider name; for the available providers and how to configure them see [README](https://github.com/kubernetes-sigs/external-dns/blob/master/charts/external-dns/README.md#providers). | | provider.webhook.args | list | `[]` | Extra arguments to provide for the `webhook` container. | diff --git a/charts/external-dns/values.schema.json b/charts/external-dns/values.schema.json index a4de3f455..75e7fdd42 100644 --- a/charts/external-dns/values.schema.json +++ b/charts/external-dns/values.schema.json @@ -270,12 +270,13 @@ } }, "policy": { - "description": "How DNS records are synchronized between sources and providers; available values are `sync` \u0026 `upsert-only`.", + "description": "How DNS records are synchronized between sources and providers; available values are `create-only`, `sync`, \u0026 `upsert-only`.", "default": "upsert-only", "type": [ "string" ], "enum": [ + "create-only", "sync", "upsert-only" ] diff --git a/charts/external-dns/values.yaml b/charts/external-dns/values.yaml index 8bc037974..dc5ec8d1e 100644 --- a/charts/external-dns/values.yaml +++ b/charts/external-dns/values.yaml @@ -213,8 +213,8 @@ sources: - service - ingress -# -- How DNS records are synchronized between sources and providers; available values are `sync` & `upsert-only`. -policy: upsert-only # @schema enum:[sync, upsert-only]; type:string; default: "upsert-only" +# -- How DNS records are synchronized between sources and providers; available values are `create-only`, `sync`, & `upsert-only`. +policy: upsert-only # @schema enum:[create-only, sync, upsert-only]; type:string; default: "upsert-only" # -- Specify the registry for storing ownership and labels. # Valid values are `txt`, `aws-sd`, `dynamodb` & `noop`. From 3e7c5cd2c17ed53081bc76dd004125466883e234 Mon Sep 17 00:00:00 2001 From: tom Date: Mon, 14 Jul 2025 11:48:24 +0200 Subject: [PATCH 40/49] fix(cloudflare): set comments properly (#5582) * fix(cloudflare): Set comments properly * Use cloudflare.StringPtr to enable comment removal --- pkg/apis/externaldns/types.go | 3 +-- provider/cloudflare/cloudflare.go | 23 +++++++++++++++-------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 4b21a6a83..ea809d3a5 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -115,7 +115,6 @@ type Config struct { CloudflareCustomHostnamesCertificateAuthority string CloudflareRegionalServices bool CloudflareRegionKey string - CloudflareRecordComment string CoreDNSPrefix string AkamaiServiceConsumerDomain string AkamaiClientToken string @@ -535,7 +534,7 @@ func App(cfg *Config) *kingpin.Application { app.Flag("cloudflare-dns-records-per-page", "When using the Cloudflare provider, specify how many DNS records listed per page, max possible 5,000 (default: 100)").Default(strconv.Itoa(defaultConfig.CloudflareDNSRecordsPerPage)).IntVar(&cfg.CloudflareDNSRecordsPerPage) app.Flag("cloudflare-regional-services", "When using the Cloudflare provider, specify if Regional Services feature will be used (default: disabled)").Default(strconv.FormatBool(defaultConfig.CloudflareRegionalServices)).BoolVar(&cfg.CloudflareRegionalServices) app.Flag("cloudflare-region-key", "When using the Cloudflare provider, specify the default region for Regional Services. Any value other than an empty string will enable the Regional Services feature (optional)").StringVar(&cfg.CloudflareRegionKey) - app.Flag("cloudflare-record-comment", "When using the Cloudflare provider, specify the comment for the DNS records (default: '')").Default("").StringVar(&cfg.CloudflareRecordComment) + app.Flag("cloudflare-record-comment", "When using the Cloudflare provider, specify the comment for the DNS records (default: '')").Default("").StringVar(&cfg.CloudflareDNSRecordsComment) app.Flag("coredns-prefix", "When using the CoreDNS provider, specify the prefix name").Default(defaultConfig.CoreDNSPrefix).StringVar(&cfg.CoreDNSPrefix) app.Flag("akamai-serviceconsumerdomain", "When using the Akamai provider, specify the base URL (required when --provider=akamai and edgerc-path not specified)").Default(defaultConfig.AkamaiServiceConsumerDomain).StringVar(&cfg.AkamaiServiceConsumerDomain) diff --git a/provider/cloudflare/cloudflare.go b/provider/cloudflare/cloudflare.go index 21d4a945c..8914032cc 100644 --- a/provider/cloudflare/cloudflare.go +++ b/provider/cloudflare/cloudflare.go @@ -173,15 +173,20 @@ type DNSRecordsConfig struct { } func (c *DNSRecordsConfig) trimAndValidateComment(dnsName, comment string, paidZone func(string) bool) string { - if len(comment) > freeZoneMaxCommentLength { - if !paidZone(dnsName) { - log.Warnf("DNS record comment is invalid. Trimming comment of %s. To avoid endless syncs, please set it to less than %d chars.", dnsName, freeZoneMaxCommentLength) - return comment[:freeZoneMaxCommentLength] - } else if len(comment) > paidZoneMaxCommentLength { - log.Warnf("DNS record comment is invalid. Trimming comment of %s. To avoid endless syncs, please set it to less than %d chars.", dnsName, paidZoneMaxCommentLength) - return comment[:paidZoneMaxCommentLength] - } + if len(comment) <= freeZoneMaxCommentLength { + return comment } + + maxLength := freeZoneMaxCommentLength + if paidZone(dnsName) { + maxLength = paidZoneMaxCommentLength + } + + if len(comment) > maxLength { + log.Warnf("DNS record comment is invalid. Trimming comment of %s. To avoid endless syncs, please set it to less than %d chars.", dnsName, maxLength) + return comment[:maxLength] + } + return comment } @@ -243,6 +248,7 @@ func updateDNSRecordParam(cfc cloudFlareChange) cloudflare.UpdateDNSRecordParams Type: cfc.ResourceRecord.Type, Content: cfc.ResourceRecord.Content, Priority: cfc.ResourceRecord.Priority, + Comment: cloudflare.StringPtr(cfc.ResourceRecord.Comment), } return params @@ -257,6 +263,7 @@ func getCreateDNSRecordParam(cfc cloudFlareChange) cloudflare.CreateDNSRecordPar Type: cfc.ResourceRecord.Type, Content: cfc.ResourceRecord.Content, Priority: cfc.ResourceRecord.Priority, + Comment: cfc.ResourceRecord.Comment, } return params From faeee1226ef8ba1ae0e90db817f512a20c190531 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Jul 2025 03:20:24 -0700 Subject: [PATCH 41/49] chore(deps): bump renovatebot/github-action (#5644) Bumps the dev-dependencies group with 1 update: [renovatebot/github-action](https://github.com/renovatebot/github-action). Updates `renovatebot/github-action` from 43.0.2 to 43.0.3 - [Release notes](https://github.com/renovatebot/github-action/releases) - [Changelog](https://github.com/renovatebot/github-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/renovatebot/github-action/compare/v43.0.2...v43.0.3) --- updated-dependencies: - dependency-name: renovatebot/github-action dependency-version: 43.0.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dev-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/dependency-update.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependency-update.yaml b/.github/workflows/dependency-update.yaml index 550103407..6ab818a8c 100644 --- a/.github/workflows/dependency-update.yaml +++ b/.github/workflows/dependency-update.yaml @@ -17,7 +17,7 @@ jobs: uses: actions/checkout@v4.2.2 # https://github.com/renovatebot/github-action - name: self-hosted renovate - uses: renovatebot/github-action@v43.0.2 + uses: renovatebot/github-action@v43.0.3 with: # https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication token: ${{ secrets.GITHUB_TOKEN }} From b93d1e9abb4adfdf4d4ba2cfca0bf5ed053c5f75 Mon Sep 17 00:00:00 2001 From: Ivan Ka <5395690+ivankatliarchuk@users.noreply.github.com> Date: Mon, 14 Jul 2025 20:42:27 +0100 Subject: [PATCH 42/49] feat(metrics): publish build_info metric (#5643) * feat(metrics): publish build_info metric Signed-off-by: ivan katliarchuk * feat(metrics): publish build_info metric Signed-off-by: ivan katliarchuk * feat(metrics): publish build_info metric Co-authored-by: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com> * feat(metrics): publish build_info metric Signed-off-by: ivan katliarchuk --------- Signed-off-by: ivan katliarchuk Co-authored-by: Michel Loiseleur <97035654+mloiseleur@users.noreply.github.com> --- controller/controller.go | 11 -------- docs/monitoring/metrics.md | 1 + internal/gen/docs/metrics/main_test.go | 2 +- pkg/metrics/metrics.go | 35 ++++++++++++++++++++------ pkg/metrics/models.go | 32 +++++++++++++++++++++++ pkg/metrics/models_test.go | 29 +++++++++++++++------ provider/cached_provider.go | 2 -- provider/webhook/webhook.go | 6 ----- 8 files changed, 84 insertions(+), 34 deletions(-) diff --git a/controller/controller.go b/controller/controller.go index 06562da58..220da28ce 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -37,7 +37,6 @@ import ( var ( registryErrorsTotal = metrics.NewCounterWithOpts( prometheus.CounterOpts{ - Namespace: "external_dns", Subsystem: "registry", Name: "errors_total", Help: "Number of Registry errors.", @@ -45,7 +44,6 @@ var ( ) sourceErrorsTotal = metrics.NewCounterWithOpts( prometheus.CounterOpts{ - Namespace: "external_dns", Subsystem: "source", Name: "errors_total", Help: "Number of Source errors.", @@ -53,7 +51,6 @@ var ( ) sourceEndpointsTotal = metrics.NewGaugeWithOpts( prometheus.GaugeOpts{ - Namespace: "external_dns", Subsystem: "source", Name: "endpoints_total", Help: "Number of Endpoints in all sources", @@ -61,7 +58,6 @@ var ( ) registryEndpointsTotal = metrics.NewGaugeWithOpts( prometheus.GaugeOpts{ - Namespace: "external_dns", Subsystem: "registry", Name: "endpoints_total", Help: "Number of Endpoints in the registry", @@ -69,7 +65,6 @@ var ( ) lastSyncTimestamp = metrics.NewGaugeWithOpts( prometheus.GaugeOpts{ - Namespace: "external_dns", Subsystem: "controller", Name: "last_sync_timestamp_seconds", Help: "Timestamp of last successful sync with the DNS provider", @@ -77,7 +72,6 @@ var ( ) lastReconcileTimestamp = metrics.NewGaugeWithOpts( prometheus.GaugeOpts{ - Namespace: "external_dns", Subsystem: "controller", Name: "last_reconcile_timestamp_seconds", Help: "Timestamp of last attempted sync with the DNS provider", @@ -85,7 +79,6 @@ var ( ) controllerNoChangesTotal = metrics.NewCounterWithOpts( prometheus.CounterOpts{ - Namespace: "external_dns", Subsystem: "controller", Name: "no_op_runs_total", Help: "Number of reconcile loops ending up with no changes on the DNS provider side.", @@ -108,7 +101,6 @@ var ( registryRecords = metrics.NewGaugedVectorOpts( prometheus.GaugeOpts{ - Namespace: "external_dns", Subsystem: "registry", Name: "records", Help: "Number of registry records partitioned by label name (vector).", @@ -118,7 +110,6 @@ var ( sourceRecords = metrics.NewGaugedVectorOpts( prometheus.GaugeOpts{ - Namespace: "external_dns", Subsystem: "source", Name: "records", Help: "Number of source records partitioned by label name (vector).", @@ -128,7 +119,6 @@ var ( verifiedRecords = metrics.NewGaugedVectorOpts( prometheus.GaugeOpts{ - Namespace: "external_dns", Subsystem: "controller", Name: "verified_records", Help: "Number of DNS records that exists both in source and registry (vector).", @@ -138,7 +128,6 @@ var ( consecutiveSoftErrors = metrics.NewGaugeWithOpts( prometheus.GaugeOpts{ - Namespace: "external_dns", Subsystem: "controller", Name: "consecutive_soft_errors", Help: "Number of consecutive soft errors in reconciliation loop.", diff --git a/docs/monitoring/metrics.md b/docs/monitoring/metrics.md index c80d39333..3da6d52af 100644 --- a/docs/monitoring/metrics.md +++ b/docs/monitoring/metrics.md @@ -20,6 +20,7 @@ curl https://localhost:7979/metrics | Name | Metric Type | Subsystem | Help | |:---------------------------------|:------------|:------------|:------------------------------------------------------| +| build_info | Gauge | | A metric with a constant '1' value labeled with 'version' and 'revision' of external_dns and the 'go_version', 'os' and the 'arch' used the build. | | consecutive_soft_errors | Gauge | controller | Number of consecutive soft errors in reconciliation loop. | | last_reconcile_timestamp_seconds | Gauge | controller | Timestamp of last attempted sync with the DNS provider | | last_sync_timestamp_seconds | Gauge | controller | Timestamp of last successful sync with the DNS provider | diff --git a/internal/gen/docs/metrics/main_test.go b/internal/gen/docs/metrics/main_test.go index a6d1efc27..bea1d75e8 100644 --- a/internal/gen/docs/metrics/main_test.go +++ b/internal/gen/docs/metrics/main_test.go @@ -37,7 +37,7 @@ func TestComputeMetrics(t *testing.T) { t.Errorf("Expected not empty metrics registry, got %d", len(reg.Metrics)) } - assert.Len(t, reg.Metrics, 19) + assert.Len(t, reg.Metrics, 20) } func TestGenerateMarkdownTableRenderer(t *testing.T) { diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index d65e172ea..73e6ea5d1 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -17,25 +17,44 @@ limitations under the License. package metrics import ( - "runtime" + "fmt" "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/common/version" log "github.com/sirupsen/logrus" cfg "sigs.k8s.io/external-dns/pkg/apis/externaldns" ) +const ( + Namespace = "external_dns" +) + var ( RegisterMetric = NewMetricsRegister() ) +func init() { + RegisterMetric.MustRegister(NewGaugeFuncMetric(prometheus.GaugeOpts{ + Namespace: Namespace, + Name: "build_info", + Help: fmt.Sprintf( + "A metric with a constant '1' value labeled with 'version' and 'revision' of %s and the 'go_version', 'os' and the 'arch' used the build.", + Namespace, + ), + ConstLabels: prometheus.Labels{ + "version": cfg.Version, + "revision": version.GetRevision(), + "go_version": version.GoVersion, + "os": version.GoOS, + "arch": version.GoArch, + }, + })) +} + func NewMetricsRegister() *MetricRegistry { reg := prometheus.WrapRegistererWith( - prometheus.Labels{ - "version": cfg.Version, - "arch": runtime.GOARCH, - "go_version": runtime.Version(), - }, + prometheus.Labels{}, prometheus.DefaultRegisterer) return &MetricRegistry{ Registerer: reg, @@ -54,7 +73,7 @@ func NewMetricsRegister() *MetricRegistry { // } func (m *MetricRegistry) MustRegister(cs IMetric) { switch v := cs.(type) { - case CounterMetric, GaugeMetric, CounterVecMetric, GaugeVecMetric: + case CounterMetric, GaugeMetric, CounterVecMetric, GaugeVecMetric, GaugeFuncMetric: if _, exists := m.mName[cs.Get().FQDN]; exists { return } else { @@ -70,6 +89,8 @@ func (m *MetricRegistry) MustRegister(cs IMetric) { m.Registerer.MustRegister(metric.Gauge) case CounterVecMetric: m.Registerer.MustRegister(metric.CounterVec) + case GaugeFuncMetric: + m.Registerer.MustRegister(metric.GaugeFunc) } log.Debugf("Register metric: %s", cs.Get().FQDN) default: diff --git a/pkg/metrics/models.go b/pkg/metrics/models.go index 1aaf708e7..8c8fb4d4f 100644 --- a/pkg/metrics/models.go +++ b/pkg/metrics/models.go @@ -88,6 +88,7 @@ func (g GaugeVecMetric) SetWithLabels(value float64, lvs ...string) { } func NewGaugeWithOpts(opts prometheus.GaugeOpts) GaugeMetric { + opts.Namespace = Namespace return GaugeMetric{ Metric: Metric{ Type: "gauge", @@ -104,6 +105,7 @@ func NewGaugeWithOpts(opts prometheus.GaugeOpts) GaugeMetric { // NewGaugedVectorOpts creates a new GaugeVec based on the provided GaugeOpts and // partitioned by the given label names. func NewGaugedVectorOpts(opts prometheus.GaugeOpts, labelNames []string) GaugeVecMetric { + opts.Namespace = Namespace return GaugeVecMetric{ Metric: Metric{ Type: "gauge", @@ -118,6 +120,7 @@ func NewGaugedVectorOpts(opts prometheus.GaugeOpts, labelNames []string) GaugeVe } func NewCounterWithOpts(opts prometheus.CounterOpts) CounterMetric { + opts.Namespace = Namespace return CounterMetric{ Metric: Metric{ Type: "counter", @@ -132,6 +135,7 @@ func NewCounterWithOpts(opts prometheus.CounterOpts) CounterMetric { } func NewCounterVecWithOpts(opts prometheus.CounterOpts, labelNames []string) CounterVecMetric { + opts.Namespace = Namespace return CounterVecMetric{ Metric: Metric{ Type: "counter", @@ -144,3 +148,31 @@ func NewCounterVecWithOpts(opts prometheus.CounterOpts, labelNames []string) Cou CounterVec: prometheus.NewCounterVec(opts, labelNames), } } + +type GaugeFuncMetric struct { + Metric + GaugeFunc prometheus.GaugeFunc +} + +func (g GaugeFuncMetric) Get() *Metric { + return &g.Metric +} + +func NewGaugeFuncMetric(opts prometheus.GaugeOpts) GaugeFuncMetric { + return GaugeFuncMetric{ + Metric: Metric{ + Type: "gauge", + Name: opts.Name, + FQDN: func() string { + if opts.Subsystem != "" { + return fmt.Sprintf("%s_%s", opts.Subsystem, opts.Name) + } + return opts.Name + }(), + Namespace: opts.Namespace, + Subsystem: opts.Subsystem, + Help: opts.Help, + }, + GaugeFunc: prometheus.NewGaugeFunc(opts, func() float64 { return 1 }), + } +} diff --git a/pkg/metrics/models_test.go b/pkg/metrics/models_test.go index 70f50ea1a..738f9f6ed 100644 --- a/pkg/metrics/models_test.go +++ b/pkg/metrics/models_test.go @@ -17,18 +17,17 @@ limitations under the License. package metrics import ( + "reflect" "testing" - dto "github.com/prometheus/client_model/go" - "github.com/prometheus/client_golang/prometheus" + dto "github.com/prometheus/client_model/go" "github.com/stretchr/testify/assert" ) func TestNewGaugeWithOpts(t *testing.T) { opts := prometheus.GaugeOpts{ Name: "test_gauge", - Namespace: "test_namespace", Subsystem: "test_subsystem", Help: "This is a test gauge", } @@ -37,7 +36,7 @@ func TestNewGaugeWithOpts(t *testing.T) { assert.Equal(t, "gauge", gaugeMetric.Type) assert.Equal(t, "test_gauge", gaugeMetric.Name) - assert.Equal(t, "test_namespace", gaugeMetric.Namespace) + assert.Equal(t, Namespace, gaugeMetric.Namespace) assert.Equal(t, "test_subsystem", gaugeMetric.Subsystem) assert.Equal(t, "This is a test gauge", gaugeMetric.Help) assert.Equal(t, "test_subsystem_test_gauge", gaugeMetric.FQDN) @@ -47,7 +46,6 @@ func TestNewGaugeWithOpts(t *testing.T) { func TestNewCounterWithOpts(t *testing.T) { opts := prometheus.CounterOpts{ Name: "test_counter", - Namespace: "test_namespace", Subsystem: "test_subsystem", Help: "This is a test counter", } @@ -56,7 +54,7 @@ func TestNewCounterWithOpts(t *testing.T) { assert.Equal(t, "counter", counterMetric.Type) assert.Equal(t, "test_counter", counterMetric.Name) - assert.Equal(t, "test_namespace", counterMetric.Namespace) + assert.Equal(t, Namespace, counterMetric.Namespace) assert.Equal(t, "test_subsystem", counterMetric.Subsystem) assert.Equal(t, "This is a test counter", counterMetric.Help) assert.Equal(t, "test_subsystem_test_counter", counterMetric.FQDN) @@ -77,7 +75,7 @@ func TestNewCounterVecWithOpts(t *testing.T) { assert.Equal(t, "counter", counterVecMetric.Type) assert.Equal(t, "test_counter_vec", counterVecMetric.Name) - assert.Equal(t, "test_namespace", counterVecMetric.Namespace) + assert.Equal(t, Namespace, counterVecMetric.Namespace) assert.Equal(t, "test_subsystem", counterVecMetric.Subsystem) assert.Equal(t, "This is a test counter vector", counterVecMetric.Help) assert.Equal(t, "test_subsystem_test_counter_vec", counterVecMetric.FQDN) @@ -113,3 +111,20 @@ func TestGaugeV_SetWithLabels(t *testing.T) { assert.Len(t, m.Label, 2) } + +func TestNewBuildInfoCollector(t *testing.T) { + metric := NewGaugeFuncMetric(prometheus.GaugeOpts{ + Namespace: Namespace, + Name: "build_info", + ConstLabels: prometheus.Labels{ + "version": "0.0.1", + "goversion": "1.24", + "arch": "arm64", + }, + }) + + desc := metric.GaugeFunc.Desc() + + assert.Equal(t, "external_dns_build_info", reflect.ValueOf(desc).Elem().FieldByName("fqName").String()) + assert.Contains(t, desc.String(), "version=\"0.0.1\"") +} diff --git a/provider/cached_provider.go b/provider/cached_provider.go index 8f86d704c..ff9d2d49b 100644 --- a/provider/cached_provider.go +++ b/provider/cached_provider.go @@ -31,7 +31,6 @@ import ( var ( cachedRecordsCallsTotal = metrics.NewCounterVecWithOpts( prometheus.CounterOpts{ - Namespace: "external_dns", Subsystem: "provider", Name: "cache_records_calls", Help: "Number of calls to the provider cache Records list.", @@ -42,7 +41,6 @@ var ( ) cachedApplyChangesCallsTotal = metrics.NewCounterWithOpts( prometheus.CounterOpts{ - Namespace: "external_dns", Subsystem: "provider", Name: "cache_apply_changes_calls", Help: "Number of calls to the provider cache ApplyChanges.", diff --git a/provider/webhook/webhook.go b/provider/webhook/webhook.go index 7e5da1187..787ace17f 100644 --- a/provider/webhook/webhook.go +++ b/provider/webhook/webhook.go @@ -43,7 +43,6 @@ const ( var ( recordsErrorsGauge = metrics.NewGaugeWithOpts( prometheus.GaugeOpts{ - Namespace: "external_dns", Subsystem: "webhook_provider", Name: "records_errors_total", Help: "Errors with Records method", @@ -51,7 +50,6 @@ var ( ) recordsRequestsGauge = metrics.NewGaugeWithOpts( prometheus.GaugeOpts{ - Namespace: "external_dns", Subsystem: "webhook_provider", Name: "records_requests_total", Help: "Requests with Records method", @@ -59,7 +57,6 @@ var ( ) applyChangesErrorsGauge = metrics.NewGaugeWithOpts( prometheus.GaugeOpts{ - Namespace: "external_dns", Subsystem: "webhook_provider", Name: "applychanges_errors_total", Help: "Errors with ApplyChanges method", @@ -67,7 +64,6 @@ var ( ) applyChangesRequestsGauge = metrics.NewGaugeWithOpts( prometheus.GaugeOpts{ - Namespace: "external_dns", Subsystem: "webhook_provider", Name: "applychanges_requests_total", Help: "Requests with ApplyChanges method", @@ -75,7 +71,6 @@ var ( ) adjustEndpointsErrorsGauge = metrics.NewGaugeWithOpts( prometheus.GaugeOpts{ - Namespace: "external_dns", Subsystem: "webhook_provider", Name: "adjustendpoints_errors_total", Help: "Errors with AdjustEndpoints method", @@ -83,7 +78,6 @@ var ( ) adjustEndpointsRequestsGauge = metrics.NewGaugeWithOpts( prometheus.GaugeOpts{ - Namespace: "external_dns", Subsystem: "webhook_provider", Name: "adjustendpoints_requests_total", Help: "Requests with AdjustEndpoints method", From 1956c7e2df7807237c13de064750f003014a3bc0 Mon Sep 17 00:00:00 2001 From: Etienne Lafarge Date: Tue, 15 Jul 2025 10:46:23 +0200 Subject: [PATCH 43/49] chore: release chart for v0.18.0 (#5633) * chore: release chart for v0.18.0 * Add version change to the changelog and merge duplicated fixed entries --- charts/external-dns/CHANGELOG.md | 7 ++++--- charts/external-dns/Chart.yaml | 4 ++-- charts/external-dns/README.md | 4 ++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/charts/external-dns/CHANGELOG.md b/charts/external-dns/CHANGELOG.md index e30a440fa..b0e88965d 100644 --- a/charts/external-dns/CHANGELOG.md +++ b/charts/external-dns/CHANGELOG.md @@ -18,16 +18,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [UNRELEASED] -### Fixed - -- Fixed the lack of schema support for `create-only` dns policy in helm values ([#5627](https://github.com/kubernetes-sigs/external-dns/pull/5627)) _@coltonhughes_ +## [v1.18.0] - 2025-07-14 ### Changed - Update RBAC for `Service` source to support `EndpointSlices`. ([#5493](https://github.com/kubernetes-sigs/external-dns/pull/5493)) _@vflaux_ +- Update _ExternalDNS_ OCI image version to [v0.18.0](https://github.com/kubernetes-sigs/external-dns/releases/tag/v0.18.0). ([#5633](https://github.com/kubernetes-sigs/external-dns/pull/5633)) _@elafarge_ ### Fixed +- Fixed the lack of schema support for `create-only` dns policy in helm values ([#5627](https://github.com/kubernetes-sigs/external-dns/pull/5627)) _@coltonhughes_ - Fixed the type of `.extraContainers` from `object` to `list` (array). ([#5564](https://github.com/kubernetes-sigs/external-dns/pull/5564)) _@svengreb_ ## [v1.17.0] - 2025-06-04 @@ -279,6 +279,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 RELEASE LINKS --> [UNRELEASED]: https://github.com/kubernetes-sigs/external-dns/tree/master/charts/external-dns +[v1.18.0]: https://github.com/kubernetes-sigs/external-dns/releases/tag/external-dns-helm-chart-1.18.0 [v1.17.0]: https://github.com/kubernetes-sigs/external-dns/releases/tag/external-dns-helm-chart-1.17.0 [v1.16.1]: https://github.com/kubernetes-sigs/external-dns/releases/tag/external-dns-helm-chart-1.16.1 [v1.16.0]: https://github.com/kubernetes-sigs/external-dns/releases/tag/external-dns-helm-chart-1.16.0 diff --git a/charts/external-dns/Chart.yaml b/charts/external-dns/Chart.yaml index 89445c3ec..c1e7c6ce9 100644 --- a/charts/external-dns/Chart.yaml +++ b/charts/external-dns/Chart.yaml @@ -2,8 +2,8 @@ apiVersion: v2 name: external-dns description: ExternalDNS synchronizes exposed Kubernetes Services and Ingresses with DNS providers. type: application -version: 1.17.0 -appVersion: 0.17.0 +version: 1.18.0 +appVersion: 0.18.0 keywords: - kubernetes - k8s diff --git a/charts/external-dns/README.md b/charts/external-dns/README.md index f67502aec..33286d914 100644 --- a/charts/external-dns/README.md +++ b/charts/external-dns/README.md @@ -1,6 +1,6 @@ # external-dns -![Version: 1.17.0](https://img.shields.io/badge/Version-1.17.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.17.0](https://img.shields.io/badge/AppVersion-0.17.0-informational?style=flat-square) +![Version: 1.18.0](https://img.shields.io/badge/Version-1.18.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 0.18.0](https://img.shields.io/badge/AppVersion-0.18.0-informational?style=flat-square) ExternalDNS synchronizes exposed Kubernetes Services and Ingresses with DNS providers. @@ -27,7 +27,7 @@ helm repo add external-dns https://kubernetes-sigs.github.io/external-dns/ After you've installed the repo you can install the chart. ```shell -helm upgrade --install external-dns external-dns/external-dns --version 1.17.0 +helm upgrade --install external-dns external-dns/external-dns --version 1.18.0 ``` ## Providers From 6e1651a21c3514f4e63baa2c04ed340b5e1eb0ed Mon Sep 17 00:00:00 2001 From: Ivan Ka <5395690+ivankatliarchuk@users.noreply.github.com> Date: Wed, 16 Jul 2025 07:50:23 +0100 Subject: [PATCH 44/49] feat(source): support --event flags with sources pod and node (#5642) Signed-off-by: ivan katliarchuk --- source/node.go | 12 +++--------- source/node_test.go | 36 ++++++++++++++++++++++++++++++++++++ source/pod.go | 18 ++++-------------- source/pod_test.go | 37 +++++++++++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 23 deletions(-) diff --git a/source/node.go b/source/node.go index 53a438d4c..28c879f94 100644 --- a/source/node.go +++ b/source/node.go @@ -27,7 +27,6 @@ import ( kubeinformers "k8s.io/client-go/informers" coreinformers "k8s.io/client-go/informers/core/v1" "k8s.io/client-go/kubernetes" - "k8s.io/client-go/tools/cache" "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/source/annotations" @@ -69,13 +68,7 @@ func NewNodeSource( nodeInformer := informerFactory.Core().V1().Nodes() // Add default resource event handler to properly initialize informer. - nodeInformer.Informer().AddEventHandler( - cache.ResourceEventHandlerFuncs{ - AddFunc: func(obj interface{}) { - log.Debug("node added") - }, - }, - ) + _, _ = nodeInformer.Informer().AddEventHandler(informers.DefaultEventHandler()) informerFactory.Start(ctx.Done()) @@ -172,7 +165,8 @@ func (ns *nodeSource) Endpoints(_ context.Context) ([]*endpoint.Endpoint, error) return endpointsSlice, nil } -func (ns *nodeSource) AddEventHandler(_ context.Context, _ func()) { +func (ns *nodeSource) AddEventHandler(_ context.Context, handler func()) { + _, _ = ns.nodeInformer.Informer().AddEventHandler(eventHandlerFunc(handler)) } // nodeAddress returns the node's externalIP and if that's not found, the node's internalIP diff --git a/source/node_test.go b/source/node_test.go index 160d5994b..b362cdfd8 100644 --- a/source/node_test.go +++ b/source/node_test.go @@ -26,7 +26,10 @@ import ( log "github.com/sirupsen/logrus" "github.com/sirupsen/logrus/hooks/test" + "github.com/stretchr/testify/mock" "k8s.io/client-go/kubernetes" + corev1lister "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" "sigs.k8s.io/external-dns/internal/testutils" @@ -619,6 +622,39 @@ func TestResourceLabelIsSetForEachNodeEndpoint(t *testing.T) { } } +func TestNodeSource_AddEventHandler(t *testing.T) { + fakeInformer := new(fakeNodeInformer) + inf := testInformer{} + fakeInformer.On("Informer").Return(&inf) + + nSource := &nodeSource{ + nodeInformer: fakeInformer, + } + + handlerCalled := false + handler := func() { handlerCalled = true } + + nSource.AddEventHandler(t.Context(), handler) + + fakeInformer.AssertNumberOfCalls(t, "Informer", 1) + assert.False(t, handlerCalled) + assert.Equal(t, 1, inf.times) +} + +type fakeNodeInformer struct { + mock.Mock + informer cache.SharedIndexInformer +} + +func (f *fakeNodeInformer) Informer() cache.SharedIndexInformer { + args := f.Called() + return args.Get(0).(cache.SharedIndexInformer) +} + +func (f *fakeNodeInformer) Lister() corev1lister.NodeLister { + return corev1lister.NewNodeLister(f.Informer().GetIndexer()) +} + type nodeListBuilder struct { nodes []v1.Node } diff --git a/source/pod.go b/source/pod.go index 25597fdeb..baa0e58e5 100644 --- a/source/pod.go +++ b/source/pod.go @@ -29,7 +29,6 @@ import ( kubeinformers "k8s.io/client-go/informers" coreinformers "k8s.io/client-go/informers/core/v1" "k8s.io/client-go/kubernetes" - "k8s.io/client-go/tools/cache" "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/source/annotations" @@ -76,18 +75,8 @@ func NewPodSource( return nil, fmt.Errorf("failed to add indexers to pod informer: %w", err) } - _, _ = podInformer.Informer().AddEventHandler( - cache.ResourceEventHandlerFuncs{ - AddFunc: func(obj interface{}) { - }, - }, - ) - _, _ = nodeInformer.Informer().AddEventHandler( - cache.ResourceEventHandlerFuncs{ - AddFunc: func(obj interface{}) { - }, - }, - ) + _, _ = podInformer.Informer().AddEventHandler(informers.DefaultEventHandler()) + _, _ = nodeInformer.Informer().AddEventHandler(informers.DefaultEventHandler()) informerFactory.Start(ctx.Done()) @@ -114,7 +103,8 @@ func NewPodSource( }, nil } -func (*podSource) AddEventHandler(_ context.Context, _ func()) { +func (ps *podSource) AddEventHandler(_ context.Context, handler func()) { + _, _ = ps.podInformer.Informer().AddEventHandler(eventHandlerFunc(handler)) } func (ps *podSource) Endpoints(_ context.Context) ([]*endpoint.Endpoint, error) { diff --git a/source/pod_test.go b/source/pod_test.go index f2d67da5a..e6e0f8b06 100644 --- a/source/pod_test.go +++ b/source/pod_test.go @@ -23,9 +23,13 @@ import ( "testing" log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + corev1lister "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/internal/testutils" @@ -909,6 +913,39 @@ func TestPodSourceLogs(t *testing.T) { } } +func TestPodSource_AddEventHandler(t *testing.T) { + fakeInformer := new(fakePodInformer) + inf := testInformer{} + fakeInformer.On("Informer").Return(&inf) + + pSource := &podSource{ + podInformer: fakeInformer, + } + + handlerCalled := false + handler := func() { handlerCalled = true } + + pSource.AddEventHandler(t.Context(), handler) + + fakeInformer.AssertNumberOfCalls(t, "Informer", 1) + assert.False(t, handlerCalled) + assert.Equal(t, 1, inf.times) +} + +type fakePodInformer struct { + mock.Mock + informer cache.SharedIndexInformer +} + +func (f *fakePodInformer) Informer() cache.SharedIndexInformer { + args := f.Called() + return args.Get(0).(cache.SharedIndexInformer) +} + +func (f *fakePodInformer) Lister() corev1lister.PodLister { + return corev1lister.NewPodLister(f.Informer().GetIndexer()) +} + func nodesFixturesIPv6() []*corev1.Node { return []*corev1.Node{ { From c4db11af1b724815f22ac781ee5e11f9a72c7686 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Jul 2025 00:20:26 -0700 Subject: [PATCH 45/49] chore(deps): bump the dev-dependencies group across 1 directory with 8 updates (#5658) Bumps the dev-dependencies group with 7 updates in the / directory: | Package | From | To | | --- | --- | --- | | [github.com/F5Networks/k8s-bigip-ctlr/v2](https://github.com/F5Networks/k8s-bigip-ctlr) | `2.20.0` | `2.20.1` | | [github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue](https://github.com/aws/aws-sdk-go-v2) | `1.19.4` | `1.19.5` | | [github.com/digitalocean/godo](https://github.com/digitalocean/godo) | `1.157.0` | `1.158.0` | | [github.com/oracle/oci-go-sdk/v65](https://github.com/oracle/oci-go-sdk) | `65.95.1` | `65.95.2` | | [google.golang.org/api](https://github.com/googleapis/google-api-go-client) | `0.241.0` | `0.242.0` | | [k8s.io/api](https://github.com/kubernetes/api) | `0.33.2` | `0.33.3` | | [k8s.io/client-go](https://github.com/kubernetes/client-go) | `0.33.2` | `0.33.3` | Updates `github.com/F5Networks/k8s-bigip-ctlr/v2` from 2.20.0 to 2.20.1 - [Release notes](https://github.com/F5Networks/k8s-bigip-ctlr/releases) - [Changelog](https://github.com/F5Networks/k8s-bigip-ctlr/blob/master/docs/RELEASE-NOTES.rst) - [Commits](https://github.com/F5Networks/k8s-bigip-ctlr/compare/v2.20.0...v2.20.1) Updates `github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue` from 1.19.4 to 1.19.5 - [Release notes](https://github.com/aws/aws-sdk-go-v2/releases) - [Changelog](https://github.com/aws/aws-sdk-go-v2/blob/main/changelog-template.json) - [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/mq/v1.19.4...service/mq/v1.19.5) Updates `github.com/digitalocean/godo` from 1.157.0 to 1.158.0 - [Release notes](https://github.com/digitalocean/godo/releases) - [Changelog](https://github.com/digitalocean/godo/blob/main/CHANGELOG.md) - [Commits](https://github.com/digitalocean/godo/compare/v1.157.0...v1.158.0) Updates `github.com/oracle/oci-go-sdk/v65` from 65.95.1 to 65.95.2 - [Release notes](https://github.com/oracle/oci-go-sdk/releases) - [Changelog](https://github.com/oracle/oci-go-sdk/blob/master/CHANGELOG.md) - [Commits](https://github.com/oracle/oci-go-sdk/compare/v65.95.1...v65.95.2) Updates `google.golang.org/api` from 0.241.0 to 0.242.0 - [Release notes](https://github.com/googleapis/google-api-go-client/releases) - [Changelog](https://github.com/googleapis/google-api-go-client/blob/main/CHANGES.md) - [Commits](https://github.com/googleapis/google-api-go-client/compare/v0.241.0...v0.242.0) Updates `k8s.io/api` from 0.33.2 to 0.33.3 - [Commits](https://github.com/kubernetes/api/compare/v0.33.2...v0.33.3) Updates `k8s.io/apimachinery` from 0.33.2 to 0.33.3 - [Commits](https://github.com/kubernetes/apimachinery/compare/v0.33.2...v0.33.3) Updates `k8s.io/client-go` from 0.33.2 to 0.33.3 - [Changelog](https://github.com/kubernetes/client-go/blob/master/CHANGELOG.md) - [Commits](https://github.com/kubernetes/client-go/compare/v0.33.2...v0.33.3) --- updated-dependencies: - dependency-name: github.com/F5Networks/k8s-bigip-ctlr/v2 dependency-version: 2.20.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue dependency-version: 1.19.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: github.com/digitalocean/godo dependency-version: 1.158.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: github.com/oracle/oci-go-sdk/v65 dependency-version: 65.95.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: google.golang.org/api dependency-version: 0.242.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: k8s.io/api dependency-version: 0.33.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: k8s.io/apimachinery dependency-version: 0.33.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: k8s.io/client-go dependency-version: 0.33.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dev-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 20 ++++++++++---------- go.sum | 36 ++++++++++++++++++------------------ 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/go.mod b/go.mod index f2f935756..7190d44ea 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 - github.com/F5Networks/k8s-bigip-ctlr/v2 v2.20.0 + github.com/F5Networks/k8s-bigip-ctlr/v2 v2.20.1 github.com/Yamashou/gqlgenc v0.33.0 github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 github.com/alecthomas/kingpin/v2 v2.4.0 @@ -16,7 +16,7 @@ require ( github.com/aws/aws-sdk-go-v2 v1.36.5 github.com/aws/aws-sdk-go-v2/config v1.29.17 github.com/aws/aws-sdk-go-v2/credentials v1.17.70 - github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.4 + github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.5 github.com/aws/aws-sdk-go-v2/service/dynamodb v1.44.0 github.com/aws/aws-sdk-go-v2/service/route53 v1.53.0 github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.35.7 @@ -29,7 +29,7 @@ require ( github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381 github.com/datawire/ambassador v1.12.4 github.com/denverdino/aliyungo v0.0.0-20230411124812-ab98a9173ace - github.com/digitalocean/godo v1.157.0 + github.com/digitalocean/godo v1.158.0 github.com/dnsimple/dnsimple-go v1.7.0 github.com/exoscale/egoscale v0.102.3 github.com/ffledgling/pdns-go v0.0.0-20180219074714-524e7daccd99 @@ -44,13 +44,14 @@ require ( github.com/miekg/dns v1.1.67 github.com/openshift/api v0.0.0-20230607130528-611114dca681 github.com/openshift/client-go v0.0.0-20230607134213-3cd0021bbee3 - github.com/oracle/oci-go-sdk/v65 v65.95.1 + github.com/oracle/oci-go-sdk/v65 v65.95.2 github.com/ovh/go-ovh v1.9.0 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pluralsh/gqlclient v1.12.2 github.com/projectcontour/contour v1.32.0 github.com/prometheus/client_golang v1.22.0 github.com/prometheus/client_model v0.6.2 + github.com/prometheus/common v0.64.0 github.com/scaleway/scaleway-sdk-go v1.0.0-beta.34 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.10.0 @@ -63,13 +64,13 @@ require ( golang.org/x/sync v0.16.0 golang.org/x/text v0.27.0 golang.org/x/time v0.12.0 - google.golang.org/api v0.241.0 + google.golang.org/api v0.242.0 gopkg.in/ns1/ns1-go.v2 v2.14.4 istio.io/api v1.26.2 istio.io/client-go v1.26.2 - k8s.io/api v0.33.2 - k8s.io/apimachinery v0.33.2 - k8s.io/client-go v0.33.2 + k8s.io/api v0.33.3 + k8s.io/apimachinery v0.33.3 + k8s.io/client-go v0.33.3 k8s.io/klog/v2 v2.130.1 sigs.k8s.io/controller-runtime v0.21.0 sigs.k8s.io/gateway-api v1.3.0 @@ -89,7 +90,7 @@ require ( github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect - github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.25.6 // indirect + github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.26.0 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 // indirect github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.17 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 // indirect @@ -154,7 +155,6 @@ require ( github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/common v0.64.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/schollz/progressbar/v3 v3.8.6 // indirect diff --git a/go.sum b/go.sum index 702b715a1..3ab0aecf1 100644 --- a/go.sum +++ b/go.sum @@ -48,8 +48,8 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DATA-DOG/go-sqlmock v1.4.1/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= -github.com/F5Networks/k8s-bigip-ctlr/v2 v2.20.0 h1:WqsoU+5aA9kDypiBzWbLSkESQUA3NDLNvkjTFzipX3I= -github.com/F5Networks/k8s-bigip-ctlr/v2 v2.20.0/go.mod h1:/lGdCgv0e1qrS4ithe2qTU6q23IT8kqZhMlFBQmuNi0= +github.com/F5Networks/k8s-bigip-ctlr/v2 v2.20.1 h1:O4a7qJCbH2bQPzsk7NNIm9/2orkYEH7g4Uerdp0gzps= +github.com/F5Networks/k8s-bigip-ctlr/v2 v2.20.1/go.mod h1:/lGdCgv0e1qrS4ithe2qTU6q23IT8kqZhMlFBQmuNi0= github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E= @@ -120,8 +120,8 @@ github.com/aws/aws-sdk-go-v2/config v1.29.17 h1:jSuiQ5jEe4SAMH6lLRMY9OVC+TqJLP56 github.com/aws/aws-sdk-go-v2/config v1.29.17/go.mod h1:9P4wwACpbeXs9Pm9w1QTh6BwWwJjwYvJ1iCt5QbCXh8= github.com/aws/aws-sdk-go-v2/credentials v1.17.70 h1:ONnH5CM16RTXRkS8Z1qg7/s2eDOhHhaXVd72mmyv4/0= github.com/aws/aws-sdk-go-v2/credentials v1.17.70/go.mod h1:M+lWhhmomVGgtuPOhO85u4pEa3SmssPTdcYpP/5J/xc= -github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.4 h1:jKR2jpZqpmBSAVX7xxdOi1E3Z0E9WizMIlxlGI3Hh9o= -github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.4/go.mod h1:ATyfcCpSMZuB/rnpFcVbiqrTiFzdwcTXeVbgEk6iXbY= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.5 h1:oUEqVqonG3xuarrsze1KVJ30KagNYDemikTbdu8KlN8= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.5/go.mod h1:VNM08cHlOsIbSHRqb6D/M2L4kKXfJv3A2/f0GNbOQSc= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 h1:KAXP9JSHO1vKGCr5f4O6WmlVKLFFXgWYAGoJosorxzU= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32/go.mod h1:h4Sg6FQdexC1yYG9RDnOvLbW1a/P986++/Y/a+GyEM8= github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 h1:SsytQyTMHMDPspp+spo7XwXTP44aJZZAC7fBV2C5+5s= @@ -132,8 +132,8 @@ github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= github.com/aws/aws-sdk-go-v2/service/dynamodb v1.44.0 h1:A99gjqZDbdhjtjJVZrmVzVKO2+p3MSg35bDWtbMQVxw= github.com/aws/aws-sdk-go-v2/service/dynamodb v1.44.0/go.mod h1:mWB0GE1bqcVSvpW7OtFA0sKuHk52+IqtnsYU2jUfYAs= -github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.25.6 h1:QHaS/SHXfyNycuu4GiWb+AfW5T3bput6X5E3Ai/Q31M= -github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.25.6/go.mod h1:He/RikglWUczbkV+fkdpcV/3GdL/rTRNVy7VaUiezMo= +github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.26.0 h1:0wOCTKrmwkyC8Bk76hYH/B4IJn5MGt6gMkSXc0A2uyc= +github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.26.0/go.mod h1:He/RikglWUczbkV+fkdpcV/3GdL/rTRNVy7VaUiezMo= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 h1:CXV68E2dNqhuynZJPB80bhPQwAKqBWVer887figW6Jc= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4/go.mod h1:/xFi9KtvBXP97ppCz1TAEvU1Uf66qvid89rbem3wCzQ= github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.17 h1:x187MqiHwBGjMGAed8Y8K1VGuCtFvQvXb24r+bwmSdo= @@ -252,8 +252,8 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/digitalocean/godo v1.157.0 h1:ReELaS6FxXNf8gryUiVH0wmyUmZN8/NCmBX4gXd3F0o= -github.com/digitalocean/godo v1.157.0/go.mod h1:tYeiWY5ZXVpU48YaFv0M5irUFHXGorZpDNm7zzdWMzM= +github.com/digitalocean/godo v1.158.0 h1:XW7UlJn2X2qH8JOm6N63Hk/PjPHjLdYgG4zPaSbmbGc= +github.com/digitalocean/godo v1.158.0/go.mod h1:tYeiWY5ZXVpU48YaFv0M5irUFHXGorZpDNm7zzdWMzM= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/dnsimple/dnsimple-go v1.7.0 h1:JKu9xJtZ3SqOC+BuYgAWeab7+EEx0sz422vu8j611ZY= github.com/dnsimple/dnsimple-go v1.7.0/go.mod h1:EKpuihlWizqYafSnQHGCd/gyvy3HkEQJ7ODB4KdV8T8= @@ -821,8 +821,8 @@ github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxS github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= -github.com/oracle/oci-go-sdk/v65 v65.95.1 h1:KCYeX+c+A0ezLVgXKpEcgXNukn6wBkD9oWoSV2FlaUs= -github.com/oracle/oci-go-sdk/v65 v65.95.1/go.mod h1:u6XRPsw9tPziBh76K7GrrRXPa8P8W3BQeqJ6ZZt9VLA= +github.com/oracle/oci-go-sdk/v65 v65.95.2 h1:0HJ0AgpLydp/DtvYrF2d4str2BjXOVAeNbuW7E07g94= +github.com/oracle/oci-go-sdk/v65 v65.95.2/go.mod h1:u6XRPsw9tPziBh76K7GrrRXPa8P8W3BQeqJ6ZZt9VLA= github.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE= github.com/ovh/go-ovh v1.9.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c= github.com/oxtoacart/bpool v0.0.0-20150712133111-4e1c5567d7c2 h1:CXwSGu/LYmbjEab5aMCs5usQRVBGThelUKBNnoSOuso= @@ -1352,8 +1352,8 @@ gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZ google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.241.0 h1:QKwqWQlkc6O895LchPEDUSYr22Xp3NCxpQRiWTB6avE= -google.golang.org/api v0.241.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= +google.golang.org/api v0.242.0 h1:7Lnb1nfnpvbkCiZek6IXKdJ0MFuAZNAJKQfA1ws62xg= +google.golang.org/api v0.242.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1461,16 +1461,16 @@ istio.io/client-go v1.26.2/go.mod h1:eAImguSJPdaDiSSS2CEsywNHE8WWfqd3WfS18Rj8ynI k8s.io/api v0.18.0/go.mod h1:q2HRQkfDzHMBZL9l/y9rH63PkQl4vae0xRT+8prbrK8= k8s.io/api v0.18.2/go.mod h1:SJCWI7OLzhZSvbY7U8zwNl9UA4o1fizoug34OV/2r78= k8s.io/api v0.18.4/go.mod h1:lOIQAKYgai1+vz9J7YcDZwC26Z0zQewYOGWdyIPUUQ4= -k8s.io/api v0.33.2 h1:YgwIS5jKfA+BZg//OQhkJNIfie/kmRsO0BmNaVSimvY= -k8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs= +k8s.io/api v0.33.3 h1:SRd5t//hhkI1buzxb288fy2xvjubstenEKL9K51KBI8= +k8s.io/api v0.33.3/go.mod h1:01Y/iLUjNBM3TAvypct7DIj0M0NIZc+PzAHCIo0CYGE= k8s.io/apiextensions-apiserver v0.18.0/go.mod h1:18Cwn1Xws4xnWQNC00FLq1E350b9lUF+aOdIWDOZxgo= k8s.io/apiextensions-apiserver v0.18.2/go.mod h1:q3faSnRGmYimiocj6cHQ1I3WpLqmDgJFlKL37fC4ZvY= k8s.io/apiextensions-apiserver v0.18.4/go.mod h1:NYeyeYq4SIpFlPxSAB6jHPIdvu3hL0pc36wuRChybio= k8s.io/apimachinery v0.18.0/go.mod h1:9SnR/e11v5IbyPCGbvJViimtJ0SwHG4nfZFjU77ftcA= k8s.io/apimachinery v0.18.2/go.mod h1:9SnR/e11v5IbyPCGbvJViimtJ0SwHG4nfZFjU77ftcA= k8s.io/apimachinery v0.18.4/go.mod h1:OaXp26zu/5J7p0f92ASynJa1pZo06YlV9fG7BoWbCko= -k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY= -k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/apimachinery v0.33.3 h1:4ZSrmNa0c/ZpZJhAgRdcsFcZOw1PQU1bALVQ0B3I5LA= +k8s.io/apimachinery v0.33.3/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= k8s.io/apiserver v0.18.0/go.mod h1:3S2O6FeBBd6XTo0njUrLxiqk8GNy6wWOftjhJcXYnjw= k8s.io/apiserver v0.18.2/go.mod h1:Xbh066NqrZO8cbsoenCwyDJ1OSi8Ag8I2lezeHxzwzw= k8s.io/apiserver v0.18.4/go.mod h1:q+zoFct5ABNnYkGIaGQ3bcbUNdmPyOCoEBcg51LChY8= @@ -1479,8 +1479,8 @@ k8s.io/cli-runtime v0.18.4/go.mod h1:9/hS/Cuf7NVzWR5F/5tyS6xsnclxoPLVtwhnkJG1Y4g k8s.io/client-go v0.18.0/go.mod h1:uQSYDYs4WhVZ9i6AIoEZuwUggLVEF64HOD37boKAtF8= k8s.io/client-go v0.18.2/go.mod h1:Xcm5wVGXX9HAA2JJ2sSBUn3tCJ+4SVlCbl2MNNv+CIU= k8s.io/client-go v0.18.4/go.mod h1:f5sXwL4yAZRkAtzOxRWUhA/N8XzGCb+nPZI8PfobZ9g= -k8s.io/client-go v0.33.2 h1:z8CIcc0P581x/J1ZYf4CNzRKxRvQAwoAolYPbtQes+E= -k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo= +k8s.io/client-go v0.33.3 h1:M5AfDnKfYmVJif92ngN532gFqakcGi6RvaOF16efrpA= +k8s.io/client-go v0.33.3/go.mod h1:luqKBQggEf3shbxHY4uVENAxrDISLOarxpTKMiUuujg= k8s.io/code-generator v0.18.0/go.mod h1:+UHX5rSbxmR8kzS+FAv7um6dtYrZokQvjHpDSYRVkTc= k8s.io/code-generator v0.18.2/go.mod h1:+UHX5rSbxmR8kzS+FAv7um6dtYrZokQvjHpDSYRVkTc= k8s.io/code-generator v0.18.4/go.mod h1:TgNEVx9hCyPGpdtCWA34olQYLkh3ok9ar7XfSsr8b6c= From 48760e653b29b78009ccfcdd6c83d8f9fac82f56 Mon Sep 17 00:00:00 2001 From: Ivan Ka <5395690+ivankatliarchuk@users.noreply.github.com> Date: Fri, 18 Jul 2025 18:16:25 +0100 Subject: [PATCH 46/49] fix(instrumented_http): migrate to own http instrumenter (#5650) Signed-off-by: ivan katliarchuk --- go.mod | 1 - go.sum | 2 - pkg/http/http.go | 97 +++++++++++++++++++++++++++++++++++++ pkg/http/http_test.go | 81 +++++++++++++++++++++++++++++++ provider/aws/config.go | 11 ++--- provider/google/google.go | 11 ++--- provider/pihole/client.go | 6 ++- provider/pihole/clientV6.go | 11 +++-- source/store.go | 14 +++--- 9 files changed, 200 insertions(+), 34 deletions(-) create mode 100644 pkg/http/http.go create mode 100644 pkg/http/http_test.go diff --git a/go.mod b/go.mod index 7190d44ea..498acdebf 100644 --- a/go.mod +++ b/go.mod @@ -38,7 +38,6 @@ require ( github.com/goccy/go-yaml v1.18.0 github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 - github.com/linki/instrumented_http v0.3.0 github.com/linode/linodego v1.53.0 github.com/maxatome/go-testdeep v1.14.0 github.com/miekg/dns v1.1.67 diff --git a/go.sum b/go.sum index 3ab0aecf1..b7f9df376 100644 --- a/go.sum +++ b/go.sum @@ -676,8 +676,6 @@ github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= -github.com/linki/instrumented_http v0.3.0 h1:dsN92+mXpfZtjJraartcQ99jnuw7fqsnPDjr85ma2dA= -github.com/linki/instrumented_http v0.3.0/go.mod h1:pjYbItoegfuVi2GUOMhEqzvm/SJKuEL3H0tc8QRLRFk= github.com/linode/linodego v1.53.0 h1:UWr7bUUVMtcfsuapC+6blm6+jJLPd7Tf9MZUpdOERnI= github.com/linode/linodego v1.53.0/go.mod h1:bI949fZaVchjWyKIA08hNyvAcV6BAS+PM2op3p7PAWA= github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= diff --git a/pkg/http/http.go b/pkg/http/http.go new file mode 100644 index 000000000..b6069f1a5 --- /dev/null +++ b/pkg/http/http.go @@ -0,0 +1,97 @@ +/* +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. +*/ + +// ref: https://github.com/linki/instrumented_http/blob/master/client.go + +package http + +import ( + "fmt" + "net/http" + "strings" + "time" + + "github.com/prometheus/client_golang/prometheus" +) + +var ( + requestDuration = prometheus.NewSummaryVec( + prometheus.SummaryOpts{ + Name: "request_duration_seconds", + Help: "The HTTP request latencies in seconds.", + Subsystem: "http", + ConstLabels: prometheus.Labels{"handler": "instrumented_http"}, + Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, + }, + []string{"scheme", "host", "path", "method", "status"}, + ) +) + +func init() { + prometheus.MustRegister(requestDuration) +} + +type CustomRoundTripper struct { + next http.RoundTripper +} + +// CancelRequest is a no-op to satisfy interfaces that require it. +// https://github.com/kubernetes/client-go/blob/34f52c14eaed7e50c845cc14e85e1c4c91e77470/transport/transport.go#L346 +func (r *CustomRoundTripper) CancelRequest(_ *http.Request) { +} + +func (r *CustomRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + start := time.Now() + resp, err := r.next.RoundTrip(req) + + status := "" + if resp != nil { + status = fmt.Sprintf("%d", resp.StatusCode) + } + + labels := prometheus.Labels{ + "scheme": req.URL.Scheme, + "host": req.URL.Host, + "path": pathProcessor(req.URL.Path), + "method": req.Method, + "status": status, + } + requestDuration.With(labels).Observe(time.Since(start).Seconds()) + return resp, err +} + +func NewInstrumentedClient(next *http.Client) *http.Client { + if next == nil { + next = http.DefaultClient + } + + next.Transport = NewInstrumentedTransport(next.Transport) + + return next +} + +func NewInstrumentedTransport(next http.RoundTripper) http.RoundTripper { + if next == nil { + next = http.DefaultTransport + } + + return &CustomRoundTripper{next: next} +} + +func pathProcessor(path string) string { + parts := strings.Split(path, "/") + return parts[len(parts)-1] +} diff --git a/pkg/http/http_test.go b/pkg/http/http_test.go new file mode 100644 index 000000000..cd17f3873 --- /dev/null +++ b/pkg/http/http_test.go @@ -0,0 +1,81 @@ +/* +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 http + +import ( + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/require" +) + +type dummyTransport struct{} + +func (d *dummyTransport) RoundTrip(req *http.Request) (*http.Response, error) { + return nil, fmt.Errorf("dummy error") +} + +func TestNewInstrumentedTransport(t *testing.T) { + dt := &dummyTransport{} + rt := NewInstrumentedTransport(dt) + crt, ok := rt.(*CustomRoundTripper) + require.True(t, ok) + require.Equal(t, dt, crt.next) + + // Should default to http.DefaultTransport if nil + rt2 := NewInstrumentedTransport(nil) + crt2, ok := rt2.(*CustomRoundTripper) + require.True(t, ok) + require.Equal(t, http.DefaultTransport, crt2.next) +} + +func TestNewInstrumentedClient(t *testing.T) { + client := &http.Client{Transport: &dummyTransport{}} + result := NewInstrumentedClient(client) + require.Equal(t, client, result) + _, ok := result.Transport.(*CustomRoundTripper) + require.True(t, ok) + + // Should default to http.DefaultClient if nil + result2 := NewInstrumentedClient(nil) + require.Equal(t, http.DefaultClient, result2) + _, ok = result2.Transport.(*CustomRoundTripper) + require.True(t, ok) +} + +func TestPathProcessor(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"/foo/bar", "bar"}, + {"/foo/", ""}, + {"/", ""}, + {"", ""}, + {"/foo/bar/baz", "baz"}, + {"foo/bar", "bar"}, + {"foo", "foo"}, + {"foo/", ""}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + require.Equal(t, tt.expected, pathProcessor(tt.input)) + }) + } +} diff --git a/provider/aws/config.go b/provider/aws/config.go index 5908150e7..ecc53c904 100644 --- a/provider/aws/config.go +++ b/provider/aws/config.go @@ -20,16 +20,16 @@ import ( "context" "fmt" "net/http" - "strings" awsv2 "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/aws/retry" "github.com/aws/aws-sdk-go-v2/config" stscredsv2 "github.com/aws/aws-sdk-go-v2/credentials/stscreds" "github.com/aws/aws-sdk-go-v2/service/sts" - "github.com/linki/instrumented_http" "github.com/sirupsen/logrus" + extdnshttp "sigs.k8s.io/external-dns/pkg/http" + "sigs.k8s.io/external-dns/pkg/apis/externaldns" ) @@ -84,12 +84,7 @@ func newV2Config(awsConfig AWSSessionConfig) (awsv2.Config, error) { config.WithRetryer(func() awsv2.Retryer { return retry.AddWithMaxAttempts(retry.NewStandard(), awsConfig.APIRetries) }), - config.WithHTTPClient(instrumented_http.NewClient(&http.Client{}, &instrumented_http.Callbacks{ - PathProcessor: func(path string) string { - parts := strings.Split(path, "/") - return parts[len(parts)-1] - }, - })), + config.WithHTTPClient(extdnshttp.NewInstrumentedClient(&http.Client{})), config.WithSharedConfigProfile(awsConfig.Profile), } diff --git a/provider/google/google.go b/provider/google/google.go index 68217e37b..fd521fc0e 100644 --- a/provider/google/google.go +++ b/provider/google/google.go @@ -20,17 +20,17 @@ import ( "context" "fmt" "sort" - "strings" "time" "cloud.google.com/go/compute/metadata" - "github.com/linki/instrumented_http" log "github.com/sirupsen/logrus" "golang.org/x/oauth2/google" dns "google.golang.org/api/dns/v1" googleapi "google.golang.org/api/googleapi" "google.golang.org/api/option" + extdnshttp "sigs.k8s.io/external-dns/pkg/http" + "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/provider" @@ -131,12 +131,7 @@ func NewGoogleProvider(ctx context.Context, project string, domainFilter *endpoi return nil, err } - gcloud = instrumented_http.NewClient(gcloud, &instrumented_http.Callbacks{ - PathProcessor: func(path string) string { - parts := strings.Split(path, "/") - return parts[len(parts)-1] - }, - }) + gcloud = extdnshttp.NewInstrumentedClient(gcloud) dnsClient, err := dns.NewService(ctx, option.WithHTTPClient(gcloud)) if err != nil { diff --git a/provider/pihole/client.go b/provider/pihole/client.go index cccf85237..f133735d7 100644 --- a/provider/pihole/client.go +++ b/provider/pihole/client.go @@ -28,10 +28,11 @@ import ( "net/url" "strings" - "github.com/linki/instrumented_http" log "github.com/sirupsen/logrus" "golang.org/x/net/html" + extdnshttp "sigs.k8s.io/external-dns/pkg/http" + "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/provider" ) @@ -71,7 +72,8 @@ func newPiholeClient(cfg PiholeConfig) (piholeAPI, error) { }, }, } - cl := instrumented_http.NewClient(httpClient, &instrumented_http.Callbacks{}) + + cl := extdnshttp.NewInstrumentedClient(httpClient) p := &piholeClient{ cfg: cfg, diff --git a/provider/pihole/clientV6.go b/provider/pihole/clientV6.go index 0d220b03c..3168b497c 100644 --- a/provider/pihole/clientV6.go +++ b/provider/pihole/clientV6.go @@ -30,9 +30,10 @@ import ( "strconv" "strings" - "github.com/linki/instrumented_http" log "github.com/sirupsen/logrus" + extdnshttp "sigs.k8s.io/external-dns/pkg/http" + "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/provider" ) @@ -65,7 +66,7 @@ func newPiholeClientV6(cfg PiholeConfig) (piholeAPI, error) { }, } - cl := instrumented_http.NewClient(httpClient, &instrumented_http.Callbacks{}) + cl := extdnshttp.NewInstrumentedClient(httpClient) p := &piholeClientV6{ cfg: cfg, @@ -164,17 +165,17 @@ func (p *piholeClientV6) listRecords(ctx context.Context, rtype string) ([]*endp DNSName, Target = recs[1], recs[0] switch rtype { case endpoint.RecordTypeA: - //PiHole return A and AAAA records. Filter to only keep the A records + // PiHole return A and AAAA records. Filter to only keep the A records if !isValidIPv4(Target) { continue } case endpoint.RecordTypeAAAA: - //PiHole return A and AAAA records. Filter to only keep the AAAA records + // PiHole return A and AAAA records. Filter to only keep the AAAA records if !isValidIPv6(Target) { continue } case endpoint.RecordTypeCNAME: - //PiHole return only CNAME records. + // PiHole return only CNAME records. // CNAME format is DNSName,target, ttl? DNSName, Target = recs[0], recs[1] if len(recs) == 3 { // TTL is present diff --git a/source/store.go b/source/store.go index 8d69a31bb..c77e89c3b 100644 --- a/source/store.go +++ b/source/store.go @@ -22,12 +22,11 @@ import ( "fmt" "net/http" "os" - "strings" + "sync" "time" "github.com/cloudfoundry-community/go-cfclient" - "github.com/linki/instrumented_http" openshift "github.com/openshift/client-go/route/clientset/versioned" log "github.com/sirupsen/logrus" istioclient "istio.io/client-go/pkg/clientset/versioned" @@ -38,6 +37,8 @@ import ( "k8s.io/client-go/tools/clientcmd" gateway "sigs.k8s.io/gateway-api/pkg/client/clientset/versioned" + extdnshttp "sigs.k8s.io/external-dns/pkg/http" + "sigs.k8s.io/external-dns/pkg/apis/externaldns" ) @@ -623,14 +624,11 @@ func instrumentedRESTConfig(kubeConfig, apiServerURL string, requestTimeout time if err != nil { return nil, err } + config.WrapTransport = func(rt http.RoundTripper) http.RoundTripper { - return instrumented_http.NewTransport(rt, &instrumented_http.Callbacks{ - PathProcessor: func(path string) string { - parts := strings.Split(path, "/") - return parts[len(parts)-1] - }, - }) + return extdnshttp.NewInstrumentedTransport(rt) } + config.Timeout = requestTimeout return config, nil } From 30cbbc0e75327eba937d873ca7966e46f2e6a2a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Badst=C3=BCbner?= Date: Fri, 18 Jul 2025 19:16:32 +0200 Subject: [PATCH 47/49] refactor: handle internal IPv6 addresses on nodeport services consistently with --expose-internal-ipv6 flag (#5652) --- source/service.go | 10 ++++- source/service_test.go | 89 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 80 insertions(+), 19 deletions(-) diff --git a/source/service.go b/source/service.go index 24f3b60ee..a339b192f 100644 --- a/source/service.go +++ b/source/service.go @@ -742,13 +742,19 @@ func (sc *serviceSource) extractNodePortTargets(svc *v1.Service) (endpoint.Targe access := getAccessFromAnnotations(svc.Annotations) switch access { case "public": - return append(externalIPs, ipv6IPs...), nil + if sc.exposeInternalIPv6 { + return append(externalIPs, ipv6IPs...), nil + } + return externalIPs, nil case "private": return internalIPs, nil } if len(externalIPs) > 0 { - return append(externalIPs, ipv6IPs...), nil + if sc.exposeInternalIPv6 { + return append(externalIPs, ipv6IPs...), nil + } + return externalIPs, nil } return internalIPs, nil diff --git a/source/service_test.go b/source/service_test.go index ecbe1fc83..dd21331ef 100644 --- a/source/service_test.go +++ b/source/service_test.go @@ -1735,7 +1735,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { expected: []*endpoint.Endpoint{ {DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV}, {DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}, RecordType: endpoint.RecordTypeA}, - {DNSName: "foo.example.org", Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2"}, RecordType: endpoint.RecordTypeAAAA}, + {DNSName: "foo.example.org", Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::3"}, RecordType: endpoint.RecordTypeAAAA}, }, nodes: []*v1.Node{{ ObjectMeta: metav1.ObjectMeta{ @@ -1745,7 +1745,8 @@ func TestServiceSourceNodePortServices(t *testing.T) { Addresses: []v1.NodeAddress{ {Type: v1.NodeExternalIP, Address: "54.10.11.1"}, {Type: v1.NodeInternalIP, Address: "10.0.1.1"}, - {Type: v1.NodeInternalIP, Address: "2001:DB8::1"}, + {Type: v1.NodeExternalIP, Address: "2001:DB8::1"}, + {Type: v1.NodeInternalIP, Address: "2001:DB8::2"}, }, }, }, { @@ -1756,7 +1757,8 @@ func TestServiceSourceNodePortServices(t *testing.T) { Addresses: []v1.NodeAddress{ {Type: v1.NodeExternalIP, Address: "54.10.11.2"}, {Type: v1.NodeInternalIP, Address: "10.0.1.2"}, - {Type: v1.NodeInternalIP, Address: "2001:DB8::2"}, + {Type: v1.NodeExternalIP, Address: "2001:DB8::3"}, + {Type: v1.NodeInternalIP, Address: "2001:DB8::4"}, }, }, }}, @@ -1779,7 +1781,8 @@ func TestServiceSourceNodePortServices(t *testing.T) { Addresses: []v1.NodeAddress{ {Type: v1.NodeExternalIP, Address: "54.10.11.1"}, {Type: v1.NodeInternalIP, Address: "10.0.1.1"}, - {Type: v1.NodeInternalIP, Address: "2001:DB8::1"}, + {Type: v1.NodeExternalIP, Address: "2001:DB8::1"}, + {Type: v1.NodeInternalIP, Address: "2001:DB8::2"}, }, }, }, { @@ -1790,7 +1793,8 @@ func TestServiceSourceNodePortServices(t *testing.T) { Addresses: []v1.NodeAddress{ {Type: v1.NodeExternalIP, Address: "54.10.11.2"}, {Type: v1.NodeInternalIP, Address: "10.0.1.2"}, - {Type: v1.NodeInternalIP, Address: "2001:DB8::2"}, + {Type: v1.NodeExternalIP, Address: "2001:DB8::3"}, + {Type: v1.NodeInternalIP, Address: "2001:DB8::4"}, }, }, }}, @@ -1806,7 +1810,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { expected: []*endpoint.Endpoint{ {DNSName: "_foo._tcp.foo.bar.example.com", Targets: endpoint.Targets{"0 50 30192 foo.bar.example.com"}, RecordType: endpoint.RecordTypeSRV}, {DNSName: "foo.bar.example.com", Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}, RecordType: endpoint.RecordTypeA}, - {DNSName: "foo.bar.example.com", Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2"}, RecordType: endpoint.RecordTypeAAAA}, + {DNSName: "foo.bar.example.com", Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::3"}, RecordType: endpoint.RecordTypeAAAA}, }, nodes: []*v1.Node{{ ObjectMeta: metav1.ObjectMeta{ @@ -1816,7 +1820,8 @@ func TestServiceSourceNodePortServices(t *testing.T) { Addresses: []v1.NodeAddress{ {Type: v1.NodeExternalIP, Address: "54.10.11.1"}, {Type: v1.NodeInternalIP, Address: "10.0.1.1"}, - {Type: v1.NodeInternalIP, Address: "2001:DB8::1"}, + {Type: v1.NodeExternalIP, Address: "2001:DB8::1"}, + {Type: v1.NodeInternalIP, Address: "2001:DB8::2"}, }, }, }, { @@ -1827,7 +1832,8 @@ func TestServiceSourceNodePortServices(t *testing.T) { Addresses: []v1.NodeAddress{ {Type: v1.NodeExternalIP, Address: "54.10.11.2"}, {Type: v1.NodeInternalIP, Address: "10.0.1.2"}, - {Type: v1.NodeInternalIP, Address: "2001:DB8::2"}, + {Type: v1.NodeExternalIP, Address: "2001:DB8::3"}, + {Type: v1.NodeInternalIP, Address: "2001:DB8::4"}, }, }, }}, @@ -1880,7 +1886,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { expected: []*endpoint.Endpoint{ {DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV}, {DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.2"}, RecordType: endpoint.RecordTypeA}, - {DNSName: "foo.example.org", Targets: endpoint.Targets{"2001:DB8::2"}, RecordType: endpoint.RecordTypeAAAA}, + {DNSName: "foo.example.org", Targets: endpoint.Targets{"2001:DB8::3"}, RecordType: endpoint.RecordTypeAAAA}, }, nodes: []*v1.Node{{ ObjectMeta: metav1.ObjectMeta{ @@ -1890,7 +1896,8 @@ func TestServiceSourceNodePortServices(t *testing.T) { Addresses: []v1.NodeAddress{ {Type: v1.NodeExternalIP, Address: "54.10.11.1"}, {Type: v1.NodeInternalIP, Address: "10.0.1.1"}, - {Type: v1.NodeInternalIP, Address: "2001:DB8::1"}, + {Type: v1.NodeExternalIP, Address: "2001:DB8::1"}, + {Type: v1.NodeInternalIP, Address: "2001:DB8::2"}, }, }, }, { @@ -1901,7 +1908,8 @@ func TestServiceSourceNodePortServices(t *testing.T) { Addresses: []v1.NodeAddress{ {Type: v1.NodeExternalIP, Address: "54.10.11.2"}, {Type: v1.NodeInternalIP, Address: "10.0.1.2"}, - {Type: v1.NodeInternalIP, Address: "2001:DB8::2"}, + {Type: v1.NodeExternalIP, Address: "2001:DB8::3"}, + {Type: v1.NodeInternalIP, Address: "2001:DB8::4"}, }, }, }}, @@ -1924,7 +1932,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { expected: []*endpoint.Endpoint{ {DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV}, {DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.2"}, RecordType: endpoint.RecordTypeA}, - {DNSName: "foo.example.org", Targets: endpoint.Targets{"2001:DB8::2"}, RecordType: endpoint.RecordTypeAAAA}, + {DNSName: "foo.example.org", Targets: endpoint.Targets{"2001:DB8::3"}, RecordType: endpoint.RecordTypeAAAA}, }, nodes: []*v1.Node{{ ObjectMeta: metav1.ObjectMeta{ @@ -1934,7 +1942,8 @@ func TestServiceSourceNodePortServices(t *testing.T) { Addresses: []v1.NodeAddress{ {Type: v1.NodeExternalIP, Address: "54.10.11.1"}, {Type: v1.NodeInternalIP, Address: "10.0.1.1"}, - {Type: v1.NodeInternalIP, Address: "2001:DB8::1"}, + {Type: v1.NodeExternalIP, Address: "2001:DB8::1"}, + {Type: v1.NodeInternalIP, Address: "2001:DB8::2"}, }, }, }, { @@ -1945,7 +1954,8 @@ func TestServiceSourceNodePortServices(t *testing.T) { Addresses: []v1.NodeAddress{ {Type: v1.NodeExternalIP, Address: "54.10.11.2"}, {Type: v1.NodeInternalIP, Address: "10.0.1.2"}, - {Type: v1.NodeInternalIP, Address: "2001:DB8::2"}, + {Type: v1.NodeExternalIP, Address: "2001:DB8::3"}, + {Type: v1.NodeInternalIP, Address: "2001:DB8::4"}, }, }, }}, @@ -2098,7 +2108,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { }}, }, { - title: "access=public annotation NodePort services return an endpoint with public IP addresses of the cluster's nodes", + title: "access=public annotation NodePort services return an endpoint with external IP addresses of the cluster's nodes if exposeInternalIPv6 is unset", svcNamespace: "testing", svcName: "foo", svcType: v1.ServiceTypeNodePort, @@ -2111,7 +2121,7 @@ func TestServiceSourceNodePortServices(t *testing.T) { expected: []*endpoint.Endpoint{ {DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV}, {DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}, RecordType: endpoint.RecordTypeA}, - {DNSName: "foo.example.org", Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2"}, RecordType: endpoint.RecordTypeAAAA}, + {DNSName: "foo.example.org", Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::3"}, RecordType: endpoint.RecordTypeAAAA}, }, nodes: []*v1.Node{{ ObjectMeta: metav1.ObjectMeta{ @@ -2121,7 +2131,8 @@ func TestServiceSourceNodePortServices(t *testing.T) { Addresses: []v1.NodeAddress{ {Type: v1.NodeExternalIP, Address: "54.10.11.1"}, {Type: v1.NodeInternalIP, Address: "10.0.1.1"}, - {Type: v1.NodeInternalIP, Address: "2001:DB8::1"}, + {Type: v1.NodeExternalIP, Address: "2001:DB8::1"}, + {Type: v1.NodeInternalIP, Address: "2001:DB8::2"}, }, }, }, { @@ -2132,9 +2143,53 @@ func TestServiceSourceNodePortServices(t *testing.T) { Addresses: []v1.NodeAddress{ {Type: v1.NodeExternalIP, Address: "54.10.11.2"}, {Type: v1.NodeInternalIP, Address: "10.0.1.2"}, + {Type: v1.NodeExternalIP, Address: "2001:DB8::3"}, + {Type: v1.NodeInternalIP, Address: "2001:DB8::4"}, + }, + }, + }}, + }, + { + title: "access=public annotation NodePort services return an endpoint with public IP addresses of the cluster's nodes if exposeInternalIPv6 is set to true", + svcNamespace: "testing", + svcName: "foo", + svcType: v1.ServiceTypeNodePort, + svcTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeCluster, + labels: map[string]string{}, + annotations: map[string]string{ + hostnameAnnotationKey: "foo.example.org.", + accessAnnotationKey: "public", + }, + exposeInternalIPv6: true, + expected: []*endpoint.Endpoint{ + {DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV}, + {DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}, RecordType: endpoint.RecordTypeA}, + {DNSName: "foo.example.org", Targets: endpoint.Targets{"2001:DB8::1", "2001:DB8::2", "2001:DB8::3", "2001:DB8::4"}, RecordType: endpoint.RecordTypeAAAA}, + }, + nodes: []*v1.Node{{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + {Type: v1.NodeExternalIP, Address: "54.10.11.1"}, + {Type: v1.NodeInternalIP, Address: "10.0.1.1"}, + {Type: v1.NodeExternalIP, Address: "2001:DB8::1"}, {Type: v1.NodeInternalIP, Address: "2001:DB8::2"}, }, }, + }, { + ObjectMeta: metav1.ObjectMeta{ + Name: "node2", + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + {Type: v1.NodeExternalIP, Address: "54.10.11.2"}, + {Type: v1.NodeInternalIP, Address: "10.0.1.2"}, + {Type: v1.NodeExternalIP, Address: "2001:DB8::3"}, + {Type: v1.NodeInternalIP, Address: "2001:DB8::4"}, + }, + }, }}, }, { From 663d10c06ec540f5e23325503cf0af90ad772275 Mon Sep 17 00:00:00 2001 From: Matt Mix <36235043+mwmix@users.noreply.github.com> Date: Sat, 19 Jul 2025 07:32:26 -0400 Subject: [PATCH 48/49] fix(docs): Fixing some errors in the dev-guide example. (#5662) --- docs/contributing/dev-guide.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/contributing/dev-guide.md b/docs/contributing/dev-guide.md index a88632256..8a04c7f6f 100644 --- a/docs/contributing/dev-guide.md +++ b/docs/contributing/dev-guide.md @@ -70,11 +70,11 @@ import ( ) func TestMe(t *testing.T) { - hook := testutils.LogsUnderTestWithLogLeve(log.WarnLevel, t) + hook := testutils.LogsUnderTestWithLogLevel(log.WarnLevel, t) ... function under tests ... testutils.TestHelperLogContains("example warning message", hook, t) // provide negative assertion - testuitls.TestHelperLogNotContains("this message should not be shown", hook, t) + testutils.TestHelperLogNotContains("this message should not be shown", hook, t) } ``` From 789494f10bcb6dc429f33ab1a17de7f082ecd1d8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Jul 2025 01:18:26 -0700 Subject: [PATCH 49/49] chore(deps): bump the dev-dependencies group with 10 updates (#5668) --- updated-dependencies: - dependency-name: github.com/aws/aws-sdk-go-v2 dependency-version: 1.36.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: github.com/aws/aws-sdk-go-v2/config dependency-version: 1.29.18 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: github.com/aws/aws-sdk-go-v2/credentials dependency-version: 1.17.71 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue dependency-version: 1.19.6 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: github.com/aws/aws-sdk-go-v2/service/dynamodb dependency-version: 1.44.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: github.com/aws/aws-sdk-go-v2/service/route53 dependency-version: 1.53.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: github.com/aws/aws-sdk-go-v2/service/servicediscovery dependency-version: 1.35.8 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: github.com/aws/aws-sdk-go-v2/service/sts dependency-version: 1.34.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: dev-dependencies - dependency-name: github.com/digitalocean/godo dependency-version: 1.159.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dev-dependencies - dependency-name: github.com/prometheus/common dependency-version: 0.65.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: dev-dependencies ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 36 ++++++++++++++--------------- go.sum | 72 +++++++++++++++++++++++++++++----------------------------- 2 files changed, 54 insertions(+), 54 deletions(-) diff --git a/go.mod b/go.mod index 498acdebf..f37e59d4b 100644 --- a/go.mod +++ b/go.mod @@ -13,14 +13,14 @@ require ( github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 github.com/alecthomas/kingpin/v2 v2.4.0 github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 - github.com/aws/aws-sdk-go-v2 v1.36.5 - github.com/aws/aws-sdk-go-v2/config v1.29.17 - github.com/aws/aws-sdk-go-v2/credentials v1.17.70 - github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.5 - github.com/aws/aws-sdk-go-v2/service/dynamodb v1.44.0 - github.com/aws/aws-sdk-go-v2/service/route53 v1.53.0 - github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.35.7 - github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 + github.com/aws/aws-sdk-go-v2 v1.36.6 + github.com/aws/aws-sdk-go-v2/config v1.29.18 + github.com/aws/aws-sdk-go-v2/credentials v1.17.71 + github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.6 + github.com/aws/aws-sdk-go-v2/service/dynamodb v1.44.1 + github.com/aws/aws-sdk-go-v2/service/route53 v1.53.1 + github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.35.8 + github.com/aws/aws-sdk-go-v2/service/sts v1.34.1 github.com/bodgit/tsig v1.2.2 github.com/cenkalti/backoff/v5 v5.0.2 github.com/civo/civogo v0.6.1 @@ -29,7 +29,7 @@ require ( github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381 github.com/datawire/ambassador v1.12.4 github.com/denverdino/aliyungo v0.0.0-20230411124812-ab98a9173ace - github.com/digitalocean/godo v1.158.0 + github.com/digitalocean/godo v1.159.0 github.com/dnsimple/dnsimple-go v1.7.0 github.com/exoscale/egoscale v0.102.3 github.com/ffledgling/pdns-go v0.0.0-20180219074714-524e7daccd99 @@ -50,7 +50,7 @@ require ( github.com/projectcontour/contour v1.32.0 github.com/prometheus/client_golang v1.22.0 github.com/prometheus/client_model v0.6.2 - github.com/prometheus/common v0.64.0 + github.com/prometheus/common v0.65.0 github.com/scaleway/scaleway-sdk-go v1.0.0-beta.34 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.10.0 @@ -85,16 +85,16 @@ require ( github.com/Masterminds/semver v1.5.0 // indirect github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.33 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.37 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.37 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect - github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.26.0 // indirect + github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.26.1 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.17 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.18 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.18 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.25.6 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.4 // indirect github.com/aws/smithy-go v1.22.4 // indirect github.com/benbjohnson/clock v1.3.0 // indirect github.com/beorn7/perks v1.0.1 // indirect diff --git a/go.sum b/go.sum index b7f9df376..d5c00e4a0 100644 --- a/go.sum +++ b/go.sum @@ -114,42 +114,42 @@ github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQ github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= -github.com/aws/aws-sdk-go-v2 v1.36.5 h1:0OF9RiEMEdDdZEMqF9MRjevyxAQcf6gY+E7vwBILFj0= -github.com/aws/aws-sdk-go-v2 v1.36.5/go.mod h1:EYrzvCCN9CMUTa5+6lf6MM4tq3Zjp8UhSGR/cBsjai0= -github.com/aws/aws-sdk-go-v2/config v1.29.17 h1:jSuiQ5jEe4SAMH6lLRMY9OVC+TqJLP5655pBGjmnjr0= -github.com/aws/aws-sdk-go-v2/config v1.29.17/go.mod h1:9P4wwACpbeXs9Pm9w1QTh6BwWwJjwYvJ1iCt5QbCXh8= -github.com/aws/aws-sdk-go-v2/credentials v1.17.70 h1:ONnH5CM16RTXRkS8Z1qg7/s2eDOhHhaXVd72mmyv4/0= -github.com/aws/aws-sdk-go-v2/credentials v1.17.70/go.mod h1:M+lWhhmomVGgtuPOhO85u4pEa3SmssPTdcYpP/5J/xc= -github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.5 h1:oUEqVqonG3xuarrsze1KVJ30KagNYDemikTbdu8KlN8= -github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.5/go.mod h1:VNM08cHlOsIbSHRqb6D/M2L4kKXfJv3A2/f0GNbOQSc= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 h1:KAXP9JSHO1vKGCr5f4O6WmlVKLFFXgWYAGoJosorxzU= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32/go.mod h1:h4Sg6FQdexC1yYG9RDnOvLbW1a/P986++/Y/a+GyEM8= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 h1:SsytQyTMHMDPspp+spo7XwXTP44aJZZAC7fBV2C5+5s= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36/go.mod h1:Q1lnJArKRXkenyog6+Y+zr7WDpk4e6XlR6gs20bbeNo= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 h1:i2vNHQiXUvKhs3quBR6aqlgJaiaexz/aNvdCktW/kAM= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36/go.mod h1:UdyGa7Q91id/sdyHPwth+043HhmP6yP9MBHgbZM0xo8= +github.com/aws/aws-sdk-go-v2 v1.36.6 h1:zJqGjVbRdTPojeCGWn5IR5pbJwSQSBh5RWFTQcEQGdU= +github.com/aws/aws-sdk-go-v2 v1.36.6/go.mod h1:EYrzvCCN9CMUTa5+6lf6MM4tq3Zjp8UhSGR/cBsjai0= +github.com/aws/aws-sdk-go-v2/config v1.29.18 h1:x4T1GRPnqKV8HMJOMtNktbpQMl3bIsfx8KbqmveUO2I= +github.com/aws/aws-sdk-go-v2/config v1.29.18/go.mod h1:bvz8oXugIsH8K7HLhBv06vDqnFv3NsGDt2Znpk7zmOU= +github.com/aws/aws-sdk-go-v2/credentials v1.17.71 h1:r2w4mQWnrTMJjOyIsZtGp3R3XGY3nqHn8C26C2lQWgA= +github.com/aws/aws-sdk-go-v2/credentials v1.17.71/go.mod h1:E7VF3acIup4GB5ckzbKFrCK0vTvEQxOxgdq4U3vcMCY= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.6 h1:gBfrCR6IwAhmx+oCf9i9FJo1+Cxx5f0In+PaYQbkqbU= +github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.19.6/go.mod h1:zAO6MqUum/2yfE/Ig1LPPtzCBudQtrGBaz1gcNzgAoY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.33 h1:D9ixiWSG4lyUBL2DDNK924Px9V/NBVpML90MHqyTADY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.33/go.mod h1:caS/m4DI+cij2paz3rtProRBI4s/+TCiWoaWZuQ9010= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.37 h1:osMWfm/sC/L4tvEdQ65Gri5ZZDCUpuYJZbTTDrsn4I0= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.37/go.mod h1:ZV2/1fbjOPr4G4v38G3Ww5TBT4+hmsK45s/rxu1fGy0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.37 h1:v+X21AvTb2wZ+ycg1gx+orkB/9U6L7AOp93R7qYxsxM= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.37/go.mod h1:G0uM1kyssELxmJ2VZEfG0q2npObR3BAkF3c1VsfVnfs= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= -github.com/aws/aws-sdk-go-v2/service/dynamodb v1.44.0 h1:A99gjqZDbdhjtjJVZrmVzVKO2+p3MSg35bDWtbMQVxw= -github.com/aws/aws-sdk-go-v2/service/dynamodb v1.44.0/go.mod h1:mWB0GE1bqcVSvpW7OtFA0sKuHk52+IqtnsYU2jUfYAs= -github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.26.0 h1:0wOCTKrmwkyC8Bk76hYH/B4IJn5MGt6gMkSXc0A2uyc= -github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.26.0/go.mod h1:He/RikglWUczbkV+fkdpcV/3GdL/rTRNVy7VaUiezMo= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.44.1 h1:UoEWyfuQ/yNOuDENk5nn+AgNCH2Y5yzQEv6YbTyhIV8= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.44.1/go.mod h1:K1I47BjiTRX00pBxfJLYK80QFRcf6blev2wbjgC5Cyc= +github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.26.1 h1:WD2RDt93+IgNvlxEKkx/b3BQrpw5G/YpDHvGXweO5wE= +github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.26.1/go.mod h1:8ZWruWnVWtJwjSHEtMWFcI1W6L6PD6i+uKCJ9EiJBbE= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 h1:CXV68E2dNqhuynZJPB80bhPQwAKqBWVer887figW6Jc= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4/go.mod h1:/xFi9KtvBXP97ppCz1TAEvU1Uf66qvid89rbem3wCzQ= -github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.17 h1:x187MqiHwBGjMGAed8Y8K1VGuCtFvQvXb24r+bwmSdo= -github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.17/go.mod h1:mC9qMbA6e1pwEq6X3zDGtZRXMG2YaElJkbJlMVHLs5I= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 h1:t0E6FzREdtCsiLIoLCWsYliNsRBgyGD/MCK571qk4MI= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17/go.mod h1:ygpklyoaypuyDvOM5ujWGrYWpAK3h7ugnmKCU/76Ys4= -github.com/aws/aws-sdk-go-v2/service/route53 v1.53.0 h1:UglIEyurCqfzZkjNdYAuXUGFu/FNWMKP5eorzggvXe8= -github.com/aws/aws-sdk-go-v2/service/route53 v1.53.0/go.mod h1:wi1naoiPnCQG3cyjsivwPON1ZmQt/EJGxFqXzubBTAw= -github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.35.7 h1:1eaP4/444jrv04HhJdwTHtgnyxWgxwdLjSYBGq+oMB4= -github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.35.7/go.mod h1:czoZQabc2chvmV/ak4oGSNR9CbcUw2bef3tatmwtoIA= -github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 h1:AIRJ3lfb2w/1/8wOOSqYb9fUKGwQbtysJ2H1MofRUPg= -github.com/aws/aws-sdk-go-v2/service/sso v1.25.5/go.mod h1:b7SiVprpU+iGazDUqvRSLf5XmCdn+JtT1on7uNL6Ipc= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 h1:BpOxT3yhLwSJ77qIY3DoHAQjZsc4HEGfMCE4NGy3uFg= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3/go.mod h1:vq/GQR1gOFLquZMSrxUK/cpvKCNVYibNyJ1m7JrU88E= -github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 h1:NFOJ/NXEGV4Rq//71Hs1jC/NvPs1ezajK+yQmkwnPV0= -github.com/aws/aws-sdk-go-v2/service/sts v1.34.0/go.mod h1:7ph2tGpfQvwzgistp2+zga9f+bCjlQJPkPUmMgDSD7w= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.18 h1:QnGWwpTiazs1Y74RwA8VUfAtKuJQbnQ98DBFnSywj0s= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.18/go.mod h1:gWOI6Vb0Bbmsi0Ejvtt3RkwKpdoa/SOYTVUlzqYPRLc= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.18 h1:vvbXsA2TVO80/KT7ZqCbx934dt6PY+vQ8hZpUZ/cpYg= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.18/go.mod h1:m2JJHledjBGNMsLOF1g9gbAxprzq3KjC8e4lxtn+eWg= +github.com/aws/aws-sdk-go-v2/service/route53 v1.53.1 h1:R3nSX1hguRy6MnknHiepSvqnnL8ansFwK2hidPesAYU= +github.com/aws/aws-sdk-go-v2/service/route53 v1.53.1/go.mod h1:fmSiB4OAghn85lQgk7XN9l9bpFg5Bm1v3HuaXKytPEw= +github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.35.8 h1:PPQUm3zG6XzctspDTWC6vO3DvP/RZ+04RB11r98yb6E= +github.com/aws/aws-sdk-go-v2/service/servicediscovery v1.35.8/go.mod h1:C1n2zhotURaNj/BNgdPdhXh/i6V53rI3RmVEaNDakSM= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.6 h1:rGtWqkQbPk7Bkwuv3NzpE/scwwL9sC1Ul3tn9x83DUI= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.6/go.mod h1:u4ku9OLv4TO4bCPdxf4fA1upaMaJmP9ZijGk3AAOC6Q= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.4 h1:OV/pxyXh+eMA0TExHEC4jyWdumLxNbzz1P0zJoezkJc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.4/go.mod h1:8Mm5VGYwtm+r305FfPSuc+aFkrypeylGYhFim6XEPoc= +github.com/aws/aws-sdk-go-v2/service/sts v1.34.1 h1:aUrLQwJfZtwv3/ZNG2xRtEen+NqI3iesuacjP51Mv1s= +github.com/aws/aws-sdk-go-v2/service/sts v1.34.1/go.mod h1:3wFBZKoWnX3r+Sm7in79i54fBmNfwhdNdQuscCw7QIk= github.com/aws/smithy-go v1.22.4 h1:uqXzVZNuNexwc/xrh6Tb56u89WDlJY6HS+KC0S4QSjw= github.com/aws/smithy-go v1.22.4/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= @@ -252,8 +252,8 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/digitalocean/godo v1.158.0 h1:XW7UlJn2X2qH8JOm6N63Hk/PjPHjLdYgG4zPaSbmbGc= -github.com/digitalocean/godo v1.158.0/go.mod h1:tYeiWY5ZXVpU48YaFv0M5irUFHXGorZpDNm7zzdWMzM= +github.com/digitalocean/godo v1.159.0 h1:GQLfVueriDHYpwLzDcbydHs6nBvQBO8/r8r9imPC434= +github.com/digitalocean/godo v1.159.0/go.mod h1:tYeiWY5ZXVpU48YaFv0M5irUFHXGorZpDNm7zzdWMzM= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/dnsimple/dnsimple-go v1.7.0 h1:JKu9xJtZ3SqOC+BuYgAWeab7+EEx0sz422vu8j611ZY= github.com/dnsimple/dnsimple-go v1.7.0/go.mod h1:EKpuihlWizqYafSnQHGCd/gyvy3HkEQJ7ODB4KdV8T8= @@ -882,8 +882,8 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8 github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= -github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4= -github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= +github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= +github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=