Repository: kubernetes-sigs/external-dns

Issue:
ChangeResourceRecordSetsRecordTypes in IAM Permissions with ABAC example is missing TXT type
The example for IAM Permissions with ABAC here: https://github.com/kubernetes-sigs/external-dns/blob/07ae419795280b827d0d5c2fc19e6d5b21b5b124/docs/tutorials/aws.md?plain=1#L63 is missing TXT type, which leads to failure to create TXT records and errors such as (sanitized): 

```
level=error msg="Failure in zone management.stage.inbake.com. when submitting change batch: operation error Route 53: ChangeResourceRecordSets, https response error StatusCode: 403, RequestID: a4d8-4055-9449-4421a5c67f48, api error AccessDenied: User: arn:aws:sts::123456789012:assumed-role/external-dns-role/external-d-49df7ce5-60ec-4942-944d is not authorized to perform: route53:ChangeResourceRecordSets on resource: arn:aws:route53:::hostedzone/XXXXYYYYZZZ because no identity-based policy allows the route53:ChangeResourceRecordSets action" profile=default zoneID=/hostedzone/XXXXYYYYZZZ zoneName=mydomain.com.
```


Hints from issue comments:
@TobyTheHutt: I'll take a shot at this.

Solution patch:
diff --git a/docs/flags.md b/docs/flags.md
index 70fc393b..61ce8934 100644
--- a/docs/flags.md
+++ b/docs/flags.md
@@ -31,9 +31,9 @@
 | `--[no-]exclude-unschedulable` | Exclude nodes that are considered unschedulable (default: true) |
 | `--[no-]expose-internal-ipv6` | When using the node source, expose internal IPv6 addresses (optional, default: false) |
 | `--fqdn-template=""` | A templated string that's used to generate DNS names from sources that don't define a hostname themselves, or to add a hostname suffix when paired with the fake source (optional). Accepts comma separated list for multiple global FQDN. |
-| `--gateway-label-filter=GATEWAY-LABEL-FILTER` | Filter Gateways of Route endpoints via label selector (default: all gateways) |
-| `--gateway-name=GATEWAY-NAME` | Limit Gateways of Route endpoints to a specific name (default: all names) |
-| `--gateway-namespace=GATEWAY-NAMESPACE` | Limit Gateways of Route endpoints to a specific namespace (default: all namespaces) |
+| `--gateway-label-filter=""` | Filter Gateways of Route endpoints via label selector (default: all gateways) |
+| `--gateway-name=""` | Limit Gateways of Route endpoints to a specific name (default: all names) |
+| `--gateway-namespace=""` | Limit Gateways of Route endpoints to a specific namespace (default: all namespaces) |
 | `--[no-]ignore-hostname-annotation` | Ignore hostname annotation when generating DNS names, valid only when --fqdn-template is set (default: false) |
 | `--[no-]ignore-ingress-rules-spec` | Ignore the spec.rules section in Ingress resources (default: false) |
 | `--[no-]ignore-ingress-tls-spec` | Ignore the spec.tls section in Ingress resources (default: false) |
@@ -43,17 +43,15 @@
 | `--managed-record-types=A...` | Record types to manage; specify multiple times to include many; (default: A,AAAA,CNAME) (supported records: A, AAAA, CNAME, NS, SRV, TXT) |
 | `--namespace=""` | Limit resources queried for endpoints to a specific namespace (default: all namespaces) |
 | `--nat64-networks=NAT64-NETWORKS` | Adding an A record for each AAAA record in NAT64-enabled networks; specify multiple times for multiple possible nets (optional) |
-| `--openshift-router-name=OPENSHIFT-ROUTER-NAME` | if source is openshift-route then you can pass the ingress controller name. Based on this name external-dns will select the respective router from the route status and map that routerCanonicalHostname to the route host while creating a CNAME record. |
+| `--openshift-router-name=""` | if source is openshift-route then you can pass the ingress controller name. Based on this name external-dns will select the respective router from the route status and map that routerCanonicalHostname to the route host while creating a CNAME record. |
 | `--pod-source-domain=""` | Domain to use for pods records (optional) |
 | `--[no-]publish-host-ip` | Allow external-dns to publish host-ip for headless services (optional) |
 | `--[no-]publish-internal-services` | Allow external-dns to publish DNS records for ClusterIP services (optional) |
 | `--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-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 |
 | `--events-emit=EVENTS-EMIT` | Events that should be emitted. Specify multiple times for multiple events support (optional, default: none, expected: RecordReady, RecordDeleted, RecordError) |
-| `--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. |
 | `--domain-filter=` | Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional) |
 | `--exclude-domains=` | Exclude subdomains (optional) |
@@ -95,7 +93,7 @@
 | `--cloudflare-custom-hostnames-certificate-authority=none` | When using the Cloudflare provider with the Custom Hostnames, specify which Certificate Authority will be used. A value of none indicates no Certificate Authority will be sent to the Cloudflare API (default: none, options: google, ssl_com, lets_encrypt, none) |
 | `--cloudflare-dns-records-per-page=100` | When using the Cloudflare provider, specify how many DNS records listed per page, max possible 5,000 (default: 100) |
 | `--[no-]cloudflare-regional-services` | When using the Cloudflare provider, specify if Regional Services feature will be used (default: disabled) |
-| `--cloudflare-region-key=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) |
+| `--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) |
 | `--cloudflare-record-comment=""` | When using the Cloudflare provider, specify the comment for the DNS records (default: '') |
 | `--coredns-prefix="/skydns/"` | When using the CoreDNS provider, specify the prefix name |
 | `--akamai-serviceconsumerdomain=""` | When using the Akamai provider, specify the base URL (required when --provider=akamai and edgerc-path not specified) |
@@ -105,7 +103,7 @@
 | `--akamai-edgerc-path=""` | When using the Akamai provider, specify the .edgerc file path. Path must be reachable form invocation environment. (required when --provider=akamai and *-token, secret serviceconsumerdomain not specified) |
 | `--akamai-edgerc-section=""` | When using the Akamai provider, specify the .edgerc file path (Optional when edgerc-path is specified) |
 | `--oci-config-file="/etc/kubernetes/oci.yaml"` | When using the OCI provider, specify the OCI configuration file (required when --provider=oci |
-| `--oci-compartment-ocid=OCI-COMPARTMENT-OCID` | When using the OCI provider, specify the OCID of the OCI compartment containing all managed zones and records.  Required when using OCI IAM instance principal authentication. |
+| `--oci-compartment-ocid=""` | When using the OCI provider, specify the OCID of the OCI compartment containing all managed zones and records.  Required when using OCI IAM instance principal authentication. |
 | `--oci-zone-scope=GLOBAL` | When using OCI provider, filter for zones with this scope (optional, options: GLOBAL, PRIVATE). Defaults to GLOBAL, setting to empty value will target both. |
 | `--[no-]oci-auth-instance-principal` | When using the OCI provider, specify whether OCI IAM instance principal authentication should be used (instead of key-based auth via the OCI config file). |
 | `--oci-zones-cache-duration=0s` | When using the OCI provider, set the zones list cache TTL (0s to disable). |
@@ -119,11 +117,11 @@
 | `--[no-]pdns-skip-tls-verify` | When using the PowerDNS/PDNS provider, disable verification of any TLS certificates (optional when --provider=pdns) (default: false) |
 | `--ns1-endpoint=""` | When using the NS1 provider, specify the URL of the API endpoint to target (default: https://api.nsone.net/v1/) |
 | `--[no-]ns1-ignoressl` | When using the NS1 provider, specify whether to verify the SSL certificate (default: false) |
-| `--ns1-min-ttl=NS1-MIN-TTL` | Minimal TTL (in seconds) for records. This value will be used if the provided TTL for a service/ingress is lower than this. |
+| `--ns1-min-ttl=0` | Minimal TTL (in seconds) for records. This value will be used if the provided TTL for a service/ingress is lower than this. |
 | `--digitalocean-api-page-size=50` | Configure the page size used when querying the DigitalOcean API. |
 | `--godaddy-api-key=""` | When using the GoDaddy provider, specify the API Key (required when --provider=godaddy) |
 | `--godaddy-api-secret=""` | When using the GoDaddy provider, specify the API secret (required when --provider=godaddy) |
-| `--godaddy-api-ttl=GODADDY-API-TTL` | TTL (in seconds) for records. This value will be used if the provided TTL for a service/ingress is not provided. |
+| `--godaddy-api-ttl=0` | TTL (in seconds) for records. This value will be used if the provided TTL for a service/ingress is not provided. |
 | `--[no-]godaddy-api-ote` | When using the GoDaddy provider, use OTE api (optional, default: false, when --provider=godaddy) |
 | `--tls-ca=""` | When using TLS communication, the path to the certificate authority to verify server communications (optionally specify --tls-client-cert for two-way TLS) |
 | `--tls-client-cert=""` | When using TLS communication, the path to the certificate to present as a client (not required for TLS) |
@@ -174,7 +172,6 @@
 | `--[no-]once` | When enabled, exits the synchronization loop after the first iteration (default: disabled) |
 | `--[no-]dry-run` | When enabled, prints DNS record changes rather than actually performing them (default: disabled) |
 | `--[no-]events` | When enabled, in addition to running every interval, the reconciliation loop will get triggered when supported sources change (default: disabled) |
-| `--min-ttl=MIN-TTL` | Configure global TTL for records in duration format. This value is used when the TTL for a source is not set or set to 0. (optional; examples: 1m12s, 72s, 72) |
 | `--log-format=text` | The format in which log messages are printed (default: text, options: text, json) |
 | `--metrics-address=":7979"` | Specify where to serve the metrics and health check endpoint (default: :7979) |
 | `--log-level=info` | Set the level of logging. (default: info, options: panic, debug, info, warning, error, fatal) |
@@ -182,3 +179,5 @@
 | `--webhook-provider-read-timeout=5s` | The read timeout for the webhook provider in duration format (default: 5s) |
 | `--webhook-provider-write-timeout=10s` | The write timeout for the webhook provider in duration format (default: 10s) |
 | `--[no-]webhook-server` | When enabled, runs as a webhook server instead of a controller. (default: false). |
+| `--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) |
+| `--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) |
diff --git a/docs/tutorials/aws-sd.md b/docs/tutorials/aws-sd.md
index f68a9675..a454b9bb 100644
--- a/docs/tutorials/aws-sd.md
+++ b/docs/tutorials/aws-sd.md
@@ -74,7 +74,7 @@ Using tags, your `servicediscovery` policy can become:
         "ForAllValues:StringLike": {
           "route53:ChangeResourceRecordSetsNormalizedRecordNames": ["*example.com", "marketing.example.com", "*-beta.example.com"],
           "route53:ChangeResourceRecordSetsActions": ["CREATE", "UPSERT", "DELETE"],
-          "route53:ChangeResourceRecordSetsRecordTypes": ["A", "AAAA", "MX"]
+          "route53:ChangeResourceRecordSetsRecordTypes": ["A", "AAAA", "CNAME", "MX", "TXT"]
         }
       }
     },
diff --git a/docs/tutorials/aws.md b/docs/tutorials/aws.md
index b1479ade..8b074698 100644
--- a/docs/tutorials/aws.md
+++ b/docs/tutorials/aws.md
@@ -59,7 +59,7 @@ You can use Attribute-based access control(ABAC) for advanced deployments.
         "ForAllValues:StringLike": {
           "route53:ChangeResourceRecordSetsNormalizedRecordNames": ["*example.com", "marketing.example.com", "*-beta.example.com"],
           "route53:ChangeResourceRecordSetsActions": ["CREATE", "UPSERT", "DELETE"],
-          "route53:ChangeResourceRecordSetsRecordTypes": ["A", "AAAA", "MX"]
+          "route53:ChangeResourceRecordSetsRecordTypes": ["A", "AAAA", "CNAME", "MX", "TXT"]
         }
       }
     },
diff --git a/pkg/apis/externaldns/binders.go b/pkg/apis/externaldns/binders.go
index 70dcd1a7..04c7f78d 100644
--- a/pkg/apis/externaldns/binders.go
+++ b/pkg/apis/externaldns/binders.go
@@ -17,6 +17,8 @@ limitations under the License.
 package externaldns
 
 import (
+	"fmt"
+	"regexp"
 	"strconv"
 	"time"
 
@@ -33,6 +35,13 @@ type FlagBinder interface {
 	Int64Var(name, help string, def int64, target *int64)
 	StringsVar(name, help string, def []string, target *[]string)
 	EnumVar(name, help, def string, target *string, allowed ...string)
+	// StringsEnumVar binds a repeatable string flag with an allowed set.
+	// Implementations may not enforce allowed values.
+	StringsEnumVar(name, help string, def []string, target *[]string, allowed ...string)
+	// StringMapVar binds key=value repeatable flags into a map.
+	StringMapVar(name, help string, target *map[string]string)
+	// RegexpVar binds a regular expression value.
+	RegexpVar(name, help string, def *regexp.Regexp, target **regexp.Regexp)
 }
 
 // KingpinBinder implements FlagBinder using github.com/alecthomas/kingpin/v2.
@@ -81,6 +90,60 @@ func (b *KingpinBinder) EnumVar(name, help, def string, target *string, allowed
 	b.App.Flag(name, help).Default(def).EnumVar(target, allowed...)
 }
 
+func (b *KingpinBinder) StringsEnumVar(name, help string, def []string, target *[]string, allowed ...string) {
+	if len(def) > 0 {
+		b.App.Flag(name, help).Default(def...).EnumsVar(target, allowed...)
+		return
+	}
+	b.App.Flag(name, help).EnumsVar(target, allowed...)
+}
+
+func (b *KingpinBinder) StringMapVar(name, help string, target *map[string]string) {
+	b.App.Flag(name, help).StringMapVar(target)
+}
+
+func (b *KingpinBinder) RegexpVar(name, help string, def *regexp.Regexp, target **regexp.Regexp) {
+	defStr := ""
+	if def != nil {
+		defStr = def.String()
+	}
+	b.App.Flag(name, help).Default(defStr).RegexpVar(target)
+}
+
+type regexpValue struct {
+	target **regexp.Regexp
+}
+
+func (rv *regexpValue) String() string {
+	if rv == nil || rv.target == nil || *rv.target == nil {
+		return ""
+	}
+	return (*rv.target).String()
+}
+
+func (rv *regexpValue) Set(s string) error {
+	re, err := regexp.Compile(s)
+	if err != nil {
+		return err
+	}
+	*rv.target = re
+	return nil
+}
+
+func (rv *regexpValue) Type() string { return "regexp" }
+
+type regexpSetter interface {
+	Set(string) error
+}
+
+func setRegexpDefault(rs regexpSetter, def *regexp.Regexp, name string) {
+	if def != nil {
+		if err := rs.Set(def.String()); err != nil {
+			panic(fmt.Errorf("invalid default regexp for flag %s: %w", name, err))
+		}
+	}
+}
+
 // CobraBinder implements FlagBinder using github.com/spf13/cobra.
 type CobraBinder struct {
 	Cmd *cobra.Command
@@ -119,3 +182,20 @@ func (b *CobraBinder) StringsVar(name, help string, def []string, target *[]stri
 func (b *CobraBinder) EnumVar(name, help, def string, target *string, allowed ...string) {
 	b.Cmd.Flags().StringVar(target, name, def, help)
 }
+
+func (b *CobraBinder) StringsEnumVar(name, help string, def []string, target *[]string, allowed ...string) {
+	// pflag does not enforce enums.
+	b.Cmd.Flags().StringArrayVar(target, name, def, help)
+}
+
+func (b *CobraBinder) StringMapVar(name, help string, target *map[string]string) {
+	// Use StringToStringVar for key=value pairs.
+	b.Cmd.Flags().StringToStringVar(target, name, map[string]string{}, help)
+}
+
+func (b *CobraBinder) RegexpVar(name, help string, def *regexp.Regexp, target **regexp.Regexp) {
+	rv := &regexpValue{target: target}
+	// set default value to mirror kingpin's Default(def.String()) behavior
+	setRegexpDefault(rv, def, name)
+	b.Cmd.Flags().Var(rv, name, help)
+}
diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go
index 68ea3740..d9f8e6ef 100644
--- a/pkg/apis/externaldns/types.go
+++ b/pkg/apis/externaldns/types.go
@@ -21,7 +21,6 @@ import (
 	"os"
 	"reflect"
 	"regexp"
-	"strconv"
 	"strings"
 	"time"
 
@@ -385,6 +384,66 @@ var defaultConfig = &Config{
 	sourceWrappers:               map[string]bool{},
 }
 
+var providerNames = []string{
+	"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",
+}
+
+var allowedSources = []string{
+	"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",
+}
+
 // NewConfig returns new Config object
 func NewConfig() *Config {
 	return &Config{
@@ -482,258 +541,299 @@ func newCobraCommand(cfg *Config) *cobra.Command {
 		},
 	}
 
-	b := NewCobraBinder(cmd)
+	// Recreate a minimal post-parse validation for Cobra so it behaves like
+	// Kingpin's Required/Enum validations.
+	cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
+		// Enforce required provider (must be present) like Kingpin.
+		if cfg.Provider == "" {
+			return fmt.Errorf("--provider is required when using cobra backend")
+		}
+		validProvider := false
+		for _, p := range providerNames {
+			if p == cfg.Provider {
+				validProvider = true
+				break
+			}
+		}
+		if !validProvider {
+			return fmt.Errorf("invalid provider %q; valid values: %s", cfg.Provider, strings.Join(providerNames, ", "))
+		}
 
-	b.EnumVar("provider", "The DNS provider where the DNS records will be created.", defaultConfig.Provider, &cfg.Provider)
-	b.StringsVar("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources.", cfg.Sources, &cfg.Sources)
+		// Enforce at least one source is present and validate allowed values.
+		if len(cfg.Sources) == 0 {
+			return fmt.Errorf("--source is required when using cobra backend")
+		}
+		for _, src := range cfg.Sources {
+			valid := false
+			for _, as := range allowedSources {
+				if src == as {
+					valid = true
+					break
+				}
+			}
+			if !valid {
+				return fmt.Errorf("invalid source %q; valid values: %s", src, strings.Join(allowedSources, ", "))
+			}
+		}
 
-	b.StringVar("server", "The Kubernetes API server to connect to (default: auto-detect)", defaultConfig.APIServerURL, &cfg.APIServerURL)
-	b.StringVar("kubeconfig", "Retrieve target cluster configuration from a Kubernetes configuration file (default: auto-detect)", defaultConfig.KubeConfig, &cfg.KubeConfig)
-	b.DurationVar("request-timeout", "Request timeout when calling Kubernetes APIs. 0s means no timeout", defaultConfig.RequestTimeout, &cfg.RequestTimeout)
+		return nil
+	}
 
-	b.StringVar("namespace", "Limit resources queried for endpoints to a specific namespace (default: all namespaces)", defaultConfig.Namespace, &cfg.Namespace)
-	b.StringsVar("domain-filter", "Limit possible target zones by domain suffix (optional)", defaultConfig.DomainFilter, &cfg.DomainFilter)
-	b.StringVar("openshift-router-name", "if source is openshift-route then you can pass the ingress controller name.", cfg.OCPRouterName, &cfg.OCPRouterName)
+	b := NewCobraBinder(cmd)
+	bindFlags(b, cfg)
 
 	return cmd
 }
 
-func (cfg *Config) AddSourceWrapper(name string) {
-	if cfg.sourceWrappers == nil {
-		cfg.sourceWrappers = make(map[string]bool)
-	}
-	cfg.sourceWrappers[name] = true
-}
-
-// IsSourceWrapperInstrumented returns whether a source wrapper is enabled or not.
-func (cfg *Config) IsSourceWrapperInstrumented(name string) bool {
-	if cfg.sourceWrappers == nil {
-		return false
-	}
-	_, ok := cfg.sourceWrappers[name]
-	return ok
-}
-
-func App(cfg *Config) *kingpin.Application {
-	app := kingpin.New("external-dns", "ExternalDNS synchronizes exposed Kubernetes Services and Ingresses with DNS providers.\n\nNote that all flags may be replaced with env vars - `--flag` -> `EXTERNAL_DNS_FLAG=1` or `--flag value` -> `EXTERNAL_DNS_FLAG=value`")
-	app.Version(Version)
-	app.DefaultEnvars()
-
+func bindFlags(b FlagBinder, cfg *Config) {
 	// Flags related to Kubernetes
-	app.Flag("server", "The Kubernetes API server to connect to (default: auto-detect)").Default(defaultConfig.APIServerURL).StringVar(&cfg.APIServerURL)
-	app.Flag("kubeconfig", "Retrieve target cluster configuration from a Kubernetes configuration file (default: auto-detect)").Default(defaultConfig.KubeConfig).StringVar(&cfg.KubeConfig)
-	app.Flag("request-timeout", "Request timeout when calling Kubernetes APIs. 0s means no timeout").Default(defaultConfig.RequestTimeout.String()).DurationVar(&cfg.RequestTimeout)
-	app.Flag("resolve-service-load-balancer-hostname", "Resolve the hostname of LoadBalancer-type Service object to IP addresses in order to create DNS A/AAAA records instead of CNAMEs").BoolVar(&cfg.ResolveServiceLoadBalancerHostname)
-	app.Flag("listen-endpoint-events", "Trigger a reconcile on changes to EndpointSlices, for Service source (default: false)").BoolVar(&cfg.ListenEndpointEvents)
+	b.StringVar("server", "The Kubernetes API server to connect to (default: auto-detect)", defaultConfig.APIServerURL, &cfg.APIServerURL)
+	b.StringVar("kubeconfig", "Retrieve target cluster configuration from a Kubernetes configuration file (default: auto-detect)", defaultConfig.KubeConfig, &cfg.KubeConfig)
+	b.DurationVar("request-timeout", "Request timeout when calling Kubernetes APIs. 0s means no timeout", defaultConfig.RequestTimeout, &cfg.RequestTimeout)
+	b.BoolVar("resolve-service-load-balancer-hostname", "Resolve the hostname of LoadBalancer-type Service object to IP addresses in order to create DNS A/AAAA records instead of CNAMEs", false, &cfg.ResolveServiceLoadBalancerHostname)
+	b.BoolVar("listen-endpoint-events", "Trigger a reconcile on changes to EndpointSlices, for Service source (default: false)", false, &cfg.ListenEndpointEvents)
 
 	// Flags related to cloud foundry
-	app.Flag("cf-api-endpoint", "The fully-qualified domain name of the cloud foundry instance you are targeting").Default(defaultConfig.CFAPIEndpoint).StringVar(&cfg.CFAPIEndpoint)
-	app.Flag("cf-username", "The username to log into the cloud foundry API").Default(defaultConfig.CFUsername).StringVar(&cfg.CFUsername)
-	app.Flag("cf-password", "The password to log into the cloud foundry API").Default(defaultConfig.CFPassword).StringVar(&cfg.CFPassword)
+	b.StringVar("cf-api-endpoint", "The fully-qualified domain name of the cloud foundry instance you are targeting", defaultConfig.CFAPIEndpoint, &cfg.CFAPIEndpoint)
+	b.StringVar("cf-username", "The username to log into the cloud foundry API", defaultConfig.CFUsername, &cfg.CFUsername)
+	b.StringVar("cf-password", "The password to log into the cloud foundry API", defaultConfig.CFPassword, &cfg.CFPassword)
 
 	// Flags related to Gloo
-	app.Flag("gloo-namespace", "The Gloo Proxy namespace; specify multiple times for multiple namespaces. (default: gloo-system)").Default("gloo-system").StringsVar(&cfg.GlooNamespaces)
+	b.StringsVar("gloo-namespace", "The Gloo Proxy namespace; specify multiple times for multiple namespaces. (default: gloo-system)", []string{"gloo-system"}, &cfg.GlooNamespaces)
 
 	// Flags related to Skipper RouteGroup
-	app.Flag("skipper-routegroup-groupversion", "The resource version for skipper routegroup").Default(defaultConfig.SkipperRouteGroupVersion).StringVar(&cfg.SkipperRouteGroupVersion)
+	b.StringVar("skipper-routegroup-groupversion", "The resource version for skipper routegroup", defaultConfig.SkipperRouteGroupVersion, &cfg.SkipperRouteGroupVersion)
 
 	// Flags related to processing source
-	app.Flag("always-publish-not-ready-addresses", "Always publish also not ready addresses for headless services (optional)").BoolVar(&cfg.AlwaysPublishNotReadyAddresses)
-	app.Flag("annotation-filter", "Filter resources queried for endpoints by annotation, using label selector semantics").Default(defaultConfig.AnnotationFilter).StringVar(&cfg.AnnotationFilter)
-	app.Flag("combine-fqdn-annotation", "Combine FQDN template and Annotations instead of overwriting (default: false)").BoolVar(&cfg.CombineFQDNAndAnnotation)
-	app.Flag("compatibility", "Process annotation semantics from legacy implementations (optional, options: mate, molecule, kops-dns-controller)").Default(defaultConfig.Compatibility).EnumVar(&cfg.Compatibility, "", "mate", "molecule", "kops-dns-controller")
-	app.Flag("connector-source-server", "The server to connect for connector source, valid only when using connector source").Default(defaultConfig.ConnectorSourceServer).StringVar(&cfg.ConnectorSourceServer)
-	app.Flag("crd-source-apiversion", "API version of the CRD for crd source, e.g. `externaldns.k8s.io/v1alpha1`, valid only when using crd source").Default(defaultConfig.CRDSourceAPIVersion).StringVar(&cfg.CRDSourceAPIVersion)
-	app.Flag("crd-source-kind", "Kind of the CRD for the crd source in API group and version specified by crd-source-apiversion").Default(defaultConfig.CRDSourceKind).StringVar(&cfg.CRDSourceKind)
-	app.Flag("default-targets", "Set globally default host/IP that will apply as a target instead of source addresses. Specify multiple times for multiple targets (optional)").StringsVar(&cfg.DefaultTargets)
-	app.Flag("force-default-targets", "Force the application of --default-targets, overriding any targets provided by the source (DEPRECATED: This reverts to (improved) legacy behavior which allows empty CRD targets for migration to new state)").Default(strconv.FormatBool(defaultConfig.ForceDefaultTargets)).BoolVar(&cfg.ForceDefaultTargets)
-	app.Flag("exclude-record-types", "Record types to exclude from management; specify multiple times to exclude many; (optional)").Default().StringsVar(&cfg.ExcludeDNSRecordTypes)
-	app.Flag("exclude-target-net", "Exclude target nets (optional)").StringsVar(&cfg.ExcludeTargetNets)
-	app.Flag("exclude-unschedulable", "Exclude nodes that are considered unschedulable (default: true)").Default(strconv.FormatBool(defaultConfig.ExcludeUnschedulable)).BoolVar(&cfg.ExcludeUnschedulable)
-	app.Flag("expose-internal-ipv6", "When using the node source, expose internal IPv6 addresses (optional, default: false)").BoolVar(&cfg.ExposeInternalIPV6)
-	app.Flag("fqdn-template", "A templated string that's used to generate DNS names from sources that don't define a hostname themselves, or to add a hostname suffix when paired with the fake source (optional). Accepts comma separated list for multiple global FQDN.").Default(defaultConfig.FQDNTemplate).StringVar(&cfg.FQDNTemplate)
-	app.Flag("gateway-label-filter", "Filter Gateways of Route endpoints via label selector (default: all gateways)").StringVar(&cfg.GatewayLabelFilter)
-	app.Flag("gateway-name", "Limit Gateways of Route endpoints to a specific name (default: all names)").StringVar(&cfg.GatewayName)
-	app.Flag("gateway-namespace", "Limit Gateways of Route endpoints to a specific namespace (default: all namespaces)").StringVar(&cfg.GatewayNamespace)
-	app.Flag("ignore-hostname-annotation", "Ignore hostname annotation when generating DNS names, valid only when --fqdn-template is set (default: false)").BoolVar(&cfg.IgnoreHostnameAnnotation)
-	app.Flag("ignore-ingress-rules-spec", "Ignore the spec.rules section in Ingress resources (default: false)").BoolVar(&cfg.IgnoreIngressRulesSpec)
-	app.Flag("ignore-ingress-tls-spec", "Ignore the spec.tls section in Ingress resources (default: false)").BoolVar(&cfg.IgnoreIngressTLSSpec)
-	app.Flag("ignore-non-host-network-pods", "Ignore pods not running on host network when using pod source (default: false)").BoolVar(&cfg.IgnoreNonHostNetworkPods)
-	app.Flag("ingress-class", "Require an Ingress to have this class name; specify multiple times to allow more than one class (optional; defaults to any class)").StringsVar(&cfg.IngressClassNames)
-	app.Flag("label-filter", "Filter resources queried for endpoints by label selector; currently supported by source types crd, gateway-httproute, gateway-grpcroute, gateway-tlsroute, gateway-tcproute, gateway-udproute, ingress, node, openshift-route, service and ambassador-host").Default(defaultConfig.LabelFilter).StringVar(&cfg.LabelFilter)
+	b.BoolVar("always-publish-not-ready-addresses", "Always publish also not ready addresses for headless services (optional)", false, &cfg.AlwaysPublishNotReadyAddresses)
+	b.StringVar("annotation-filter", "Filter resources queried for endpoints by annotation, using label selector semantics", defaultConfig.AnnotationFilter, &cfg.AnnotationFilter)
+	b.BoolVar("combine-fqdn-annotation", "Combine FQDN template and Annotations instead of overwriting (default: false)", false, &cfg.CombineFQDNAndAnnotation)
+	b.EnumVar("compatibility", "Process annotation semantics from legacy implementations (optional, options: mate, molecule, kops-dns-controller)", defaultConfig.Compatibility, &cfg.Compatibility, "", "mate", "molecule", "kops-dns-controller")
+	b.StringVar("connector-source-server", "The server to connect for connector source, valid only when using connector source", defaultConfig.ConnectorSourceServer, &cfg.ConnectorSourceServer)
+	b.StringVar("crd-source-apiversion", "API version of the CRD for crd source, e.g. `externaldns.k8s.io/v1alpha1`, valid only when using crd source", defaultConfig.CRDSourceAPIVersion, &cfg.CRDSourceAPIVersion)
+	b.StringVar("crd-source-kind", "Kind of the CRD for the crd source in API group and version specified by crd-source-apiversion", defaultConfig.CRDSourceKind, &cfg.CRDSourceKind)
+	b.StringsVar("default-targets", "Set globally default host/IP that will apply as a target instead of source addresses. Specify multiple times for multiple targets (optional)", nil, &cfg.DefaultTargets)
+	b.BoolVar("force-default-targets", "Force the application of --default-targets, overriding any targets provided by the source (DEPRECATED: This reverts to (improved) legacy behavior which allows empty CRD targets for migration to new state)", defaultConfig.ForceDefaultTargets, &cfg.ForceDefaultTargets)
+	b.StringsVar("exclude-record-types", "Record types to exclude from management; specify multiple times to exclude many; (optional)", nil, &cfg.ExcludeDNSRecordTypes)
+	b.StringsVar("exclude-target-net", "Exclude target nets (optional)", nil, &cfg.ExcludeTargetNets)
+	b.BoolVar("exclude-unschedulable", "Exclude nodes that are considered unschedulable (default: true)", defaultConfig.ExcludeUnschedulable, &cfg.ExcludeUnschedulable)
+	b.BoolVar("expose-internal-ipv6", "When using the node source, expose internal IPv6 addresses (optional, default: false)", false, &cfg.ExposeInternalIPV6)
+	b.StringVar("fqdn-template", "A templated string that's used to generate DNS names from sources that don't define a hostname themselves, or to add a hostname suffix when paired with the fake source (optional). Accepts comma separated list for multiple global FQDN.", defaultConfig.FQDNTemplate, &cfg.FQDNTemplate)
+	b.StringVar("gateway-label-filter", "Filter Gateways of Route endpoints via label selector (default: all gateways)", defaultConfig.GatewayLabelFilter, &cfg.GatewayLabelFilter)
+	b.StringVar("gateway-name", "Limit Gateways of Route endpoints to a specific name (default: all names)", defaultConfig.GatewayName, &cfg.GatewayName)
+	b.StringVar("gateway-namespace", "Limit Gateways of Route endpoints to a specific namespace (default: all namespaces)", defaultConfig.GatewayNamespace, &cfg.GatewayNamespace)
+	b.BoolVar("ignore-hostname-annotation", "Ignore hostname annotation when generating DNS names, valid only when --fqdn-template is set (default: false)", false, &cfg.IgnoreHostnameAnnotation)
+	b.BoolVar("ignore-ingress-rules-spec", "Ignore the spec.rules section in Ingress resources (default: false)", false, &cfg.IgnoreIngressRulesSpec)
+	b.BoolVar("ignore-ingress-tls-spec", "Ignore the spec.tls section in Ingress resources (default: false)", false, &cfg.IgnoreIngressTLSSpec)
+	b.BoolVar("ignore-non-host-network-pods", "Ignore pods not running on host network when using pod source (default: false)", false, &cfg.IgnoreNonHostNetworkPods)
+	b.StringsVar("ingress-class", "Require an Ingress to have this class name; specify multiple times to allow more than one class (optional; defaults to any class)", nil, &cfg.IngressClassNames)
+	b.StringVar("label-filter", "Filter resources queried for endpoints by label selector; currently supported by source types crd, gateway-httproute, gateway-grpcroute, gateway-tlsroute, gateway-tcproute, gateway-udproute, ingress, node, openshift-route, service and ambassador-host", defaultConfig.LabelFilter, &cfg.LabelFilter)
 	managedRecordTypesHelp := fmt.Sprintf("Record types to manage; specify multiple times to include many; (default: %s) (supported records: A, AAAA, CNAME, NS, SRV, TXT)", strings.Join(defaultConfig.ManagedDNSRecordTypes, ","))
-	app.Flag("managed-record-types", managedRecordTypesHelp).Default(defaultConfig.ManagedDNSRecordTypes...).StringsVar(&cfg.ManagedDNSRecordTypes)
-	app.Flag("namespace", "Limit resources queried for endpoints to a specific namespace (default: all namespaces)").Default(defaultConfig.Namespace).StringVar(&cfg.Namespace)
-	app.Flag("nat64-networks", "Adding an A record for each AAAA record in NAT64-enabled networks; specify multiple times for multiple possible nets (optional)").StringsVar(&cfg.NAT64Networks)
-	app.Flag("openshift-router-name", "if source is openshift-route then you can pass the ingress controller name. Based on this name external-dns will select the respective router from the route status and map that routerCanonicalHostname to the route host while creating a CNAME record.").StringVar(&cfg.OCPRouterName)
-	app.Flag("pod-source-domain", "Domain to use for pods records (optional)").Default(defaultConfig.PodSourceDomain).StringVar(&cfg.PodSourceDomain)
-	app.Flag("publish-host-ip", "Allow external-dns to publish host-ip for headless services (optional)").BoolVar(&cfg.PublishHostIP)
-	app.Flag("publish-internal-services", "Allow external-dns to publish DNS records for ClusterIP services (optional)").BoolVar(&cfg.PublishInternal)
-	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-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)
-
-	app.Flag("events-emit", "Events that should be emitted. Specify multiple times for multiple events support (optional, default: none, expected: RecordReady, RecordDeleted, RecordError)").Default(defaultConfig.EmitEvents...).StringsVar(&cfg.EmitEvents)
+	b.StringsVar("managed-record-types", managedRecordTypesHelp, defaultConfig.ManagedDNSRecordTypes, &cfg.ManagedDNSRecordTypes)
+	b.StringVar("namespace", "Limit resources queried for endpoints to a specific namespace (default: all namespaces)", defaultConfig.Namespace, &cfg.Namespace)
+	b.StringsVar("nat64-networks", "Adding an A record for each AAAA record in NAT64-enabled networks; specify multiple times for multiple possible nets (optional)", nil, &cfg.NAT64Networks)
+	b.StringVar("openshift-router-name", "if source is openshift-route then you can pass the ingress controller name. Based on this name external-dns will select the respective router from the route status and map that routerCanonicalHostname to the route host while creating a CNAME record.", defaultConfig.OCPRouterName, &cfg.OCPRouterName)
+	b.StringVar("pod-source-domain", "Domain to use for pods records (optional)", defaultConfig.PodSourceDomain, &cfg.PodSourceDomain)
+	b.BoolVar("publish-host-ip", "Allow external-dns to publish host-ip for headless services (optional)", false, &cfg.PublishHostIP)
+	b.BoolVar("publish-internal-services", "Allow external-dns to publish DNS records for ClusterIP services (optional)", false, &cfg.PublishInternal)
+	b.StringsVar("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)", defaultConfig.ServiceTypeFilter, &cfg.ServiceTypeFilter)
+	b.StringsVar("target-net-filter", "Limit possible targets by a net filter; specify multiple times for multiple possible nets (optional)", nil, &cfg.TargetNetFilter)
+	b.BoolVar("traefik-enable-legacy", "Enable legacy listeners on Resources under the traefik.containo.us API Group", defaultConfig.TraefikEnableLegacy, &cfg.TraefikEnableLegacy)
+	b.BoolVar("traefik-disable-new", "Disable listeners on Resources under the traefik.io API Group", defaultConfig.TraefikDisableNew, &cfg.TraefikDisableNew)
+
+	b.StringsVar("events-emit", "Events that should be emitted. Specify multiple times for multiple events support (optional, default: none, expected: RecordReady, RecordDeleted, RecordError)", defaultConfig.EmitEvents, &cfg.EmitEvents)
 
 	// Flags related to providers
-	providers := []string{"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"}
-	app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: "+strings.Join(providers, ", ")+")").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, providers...)
-	app.Flag("provider-cache-time", "The time to cache the DNS provider record list requests.").Default(defaultConfig.ProviderCacheTime.String()).DurationVar(&cfg.ProviderCacheTime)
-	app.Flag("domain-filter", "Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional)").Default("").StringsVar(&cfg.DomainFilter)
-	app.Flag("exclude-domains", "Exclude subdomains (optional)").Default("").StringsVar(&cfg.ExcludeDomains)
-	app.Flag("regex-domain-filter", "Limit possible domains and target zones by a Regex filter; Overrides domain-filter (optional)").Default(defaultConfig.RegexDomainFilter.String()).RegexpVar(&cfg.RegexDomainFilter)
-	app.Flag("regex-domain-exclusion", "Regex filter that excludes domains and target zones matched by regex-domain-filter (optional); Require 'regex-domain-filter' ").Default(defaultConfig.RegexDomainExclusion.String()).RegexpVar(&cfg.RegexDomainExclusion)
-	app.Flag("zone-name-filter", "Filter target zones by zone domain (For now, only AzureDNS provider is using this flag); specify multiple times for multiple zones (optional)").Default("").StringsVar(&cfg.ZoneNameFilter)
-	app.Flag("zone-id-filter", "Filter target zones by hosted zone id; specify multiple times for multiple zones (optional)").Default("").StringsVar(&cfg.ZoneIDFilter)
-	app.Flag("google-project", "When using the Google provider, current project is auto-detected, when running on GCP. Specify other project with this. Must be specified when running outside GCP.").Default(defaultConfig.GoogleProject).StringVar(&cfg.GoogleProject)
-	app.Flag("google-batch-change-size", "When using the Google provider, set the maximum number of changes that will be applied in each batch.").Default(strconv.Itoa(defaultConfig.GoogleBatchChangeSize)).IntVar(&cfg.GoogleBatchChangeSize)
-	app.Flag("google-batch-change-interval", "When using the Google provider, set the interval between batch changes.").Default(defaultConfig.GoogleBatchChangeInterval.String()).DurationVar(&cfg.GoogleBatchChangeInterval)
-	app.Flag("google-zone-visibility", "When using the Google provider, filter for zones with this visibility (optional, options: public, private)").Default(defaultConfig.GoogleZoneVisibility).EnumVar(&cfg.GoogleZoneVisibility, "", "public", "private")
-	app.Flag("alibaba-cloud-config-file", "When using the Alibaba Cloud provider, specify the Alibaba Cloud configuration file (required when --provider=alibabacloud)").Default(defaultConfig.AlibabaCloudConfigFile).StringVar(&cfg.AlibabaCloudConfigFile)
-	app.Flag("alibaba-cloud-zone-type", "When using the Alibaba Cloud provider, filter for zones of this type (optional, options: public, private)").Default(defaultConfig.AlibabaCloudZoneType).EnumVar(&cfg.AlibabaCloudZoneType, "", "public", "private")
-	app.Flag("aws-zone-type", "When using the AWS provider, filter for zones of this type (optional, default: any, options: public, private)").Default(defaultConfig.AWSZoneType).EnumVar(&cfg.AWSZoneType, "", "public", "private")
-	app.Flag("aws-zone-tags", "When using the AWS provider, filter for zones with these tags").Default("").StringsVar(&cfg.AWSZoneTagFilter)
-	app.Flag("aws-profile", "When using the AWS provider, name of the profile to use").Default("").StringsVar(&cfg.AWSProfiles)
-	app.Flag("aws-assume-role", "When using the AWS API, assume this IAM role. Useful for hosted zones in another AWS account. Specify the full ARN, e.g. `arn:aws:iam::123455567:role/external-dns` (optional)").Default(defaultConfig.AWSAssumeRole).StringVar(&cfg.AWSAssumeRole)
-	app.Flag("aws-assume-role-external-id", "When using the AWS API and assuming a role then specify this external ID` (optional)").Default(defaultConfig.AWSAssumeRoleExternalID).StringVar(&cfg.AWSAssumeRoleExternalID)
-	app.Flag("aws-batch-change-size", "When using the AWS provider, set the maximum number of changes that will be applied in each batch.").Default(strconv.Itoa(defaultConfig.AWSBatchChangeSize)).IntVar(&cfg.AWSBatchChangeSize)
-	app.Flag("aws-batch-change-size-bytes", "When using the AWS provider, set the maximum byte size that will be applied in each batch.").Default(strconv.Itoa(defaultConfig.AWSBatchChangeSizeBytes)).IntVar(&cfg.AWSBatchChangeSizeBytes)
-	app.Flag("aws-batch-change-size-values", "When using the AWS provider, set the maximum total record values that will be applied in each batch.").Default(strconv.Itoa(defaultConfig.AWSBatchChangeSizeValues)).IntVar(&cfg.AWSBatchChangeSizeValues)
-	app.Flag("aws-batch-change-interval", "When using the AWS provider, set the interval between batch changes.").Default(defaultConfig.AWSBatchChangeInterval.String()).DurationVar(&cfg.AWSBatchChangeInterval)
-	app.Flag("aws-evaluate-target-health", "When using the AWS provider, set whether to evaluate the health of a DNS target (default: enabled, disable with --no-aws-evaluate-target-health)").Default(strconv.FormatBool(defaultConfig.AWSEvaluateTargetHealth)).BoolVar(&cfg.AWSEvaluateTargetHealth)
-	app.Flag("aws-api-retries", "When using the AWS API, set the maximum number of retries before giving up.").Default(strconv.Itoa(defaultConfig.AWSAPIRetries)).IntVar(&cfg.AWSAPIRetries)
-	app.Flag("aws-prefer-cname", "When using the AWS provider, prefer using CNAME instead of ALIAS (default: disabled)").BoolVar(&cfg.AWSPreferCNAME)
-	app.Flag("aws-zones-cache-duration", "When using the AWS provider, set the zones list cache TTL (0s to disable).").Default(defaultConfig.AWSZoneCacheDuration.String()).DurationVar(&cfg.AWSZoneCacheDuration)
-	app.Flag("aws-zone-match-parent", "Expand limit possible target by sub-domains (default: disabled)").BoolVar(&cfg.AWSZoneMatchParent)
-	app.Flag("aws-sd-service-cleanup", "When using the AWS CloudMap provider, delete empty Services without endpoints (default: disabled)").BoolVar(&cfg.AWSSDServiceCleanup)
-	app.Flag("aws-sd-create-tag", "When using the AWS CloudMap provider, add tag to created services. The flag can be used multiple times").StringMapVar(&cfg.AWSSDCreateTag)
-	app.Flag("azure-config-file", "When using the Azure provider, specify the Azure configuration file (required when --provider=azure)").Default(defaultConfig.AzureConfigFile).StringVar(&cfg.AzureConfigFile)
-	app.Flag("azure-resource-group", "When using the Azure provider, override the Azure resource group to use (optional)").Default(defaultConfig.AzureResourceGroup).StringVar(&cfg.AzureResourceGroup)
-	app.Flag("azure-subscription-id", "When using the Azure provider, override the Azure subscription to use (optional)").Default(defaultConfig.AzureSubscriptionID).StringVar(&cfg.AzureSubscriptionID)
-	app.Flag("azure-user-assigned-identity-client-id", "When using the Azure provider, override the client id of user assigned identity in config file (optional)").Default("").StringVar(&cfg.AzureUserAssignedIdentityClientID)
-	app.Flag("azure-zones-cache-duration", "When using the Azure provider, set the zones list cache TTL (0s to disable).").Default(defaultConfig.AzureZonesCacheDuration.String()).DurationVar(&cfg.AzureZonesCacheDuration)
-	app.Flag("azure-maxretries-count", "When using the Azure provider, set the number of retries for API calls (When less than 0, it disables retries). (optional)").Default(strconv.Itoa(defaultConfig.AzureMaxRetriesCount)).IntVar(&cfg.AzureMaxRetriesCount)
-
-	app.Flag("cloudflare-proxied", "When using the Cloudflare provider, specify if the proxy mode must be enabled (default: disabled)").BoolVar(&cfg.CloudflareProxied)
-	app.Flag("cloudflare-custom-hostnames", "When using the Cloudflare provider, specify if the Custom Hostnames feature will be used. Requires \"Cloudflare for SaaS\" enabled. (default: disabled)").BoolVar(&cfg.CloudflareCustomHostnames)
-	app.Flag("cloudflare-custom-hostnames-min-tls-version", "When using the Cloudflare provider with the Custom Hostnames, specify which Minimum TLS Version will be used by default. (default: 1.0, options: 1.0, 1.1, 1.2, 1.3)").Default("1.0").EnumVar(&cfg.CloudflareCustomHostnamesMinTLSVersion, "1.0", "1.1", "1.2", "1.3")
-	app.Flag("cloudflare-custom-hostnames-certificate-authority", "When using the Cloudflare provider with the Custom Hostnames, specify which Certificate Authority will be used. A value of none indicates no Certificate Authority will be sent to the Cloudflare API (default: none, options: google, ssl_com, lets_encrypt, none)").Default("none").EnumVar(&cfg.CloudflareCustomHostnamesCertificateAuthority, "google", "ssl_com", "lets_encrypt", "none")
-	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.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)
-	app.Flag("akamai-client-token", "When using the Akamai provider, specify the client token (required when --provider=akamai and edgerc-path not specified)").Default(defaultConfig.AkamaiClientToken).StringVar(&cfg.AkamaiClientToken)
-	app.Flag("akamai-client-secret", "When using the Akamai provider, specify the client secret (required when --provider=akamai and edgerc-path not specified)").Default(defaultConfig.AkamaiClientSecret).StringVar(&cfg.AkamaiClientSecret)
-	app.Flag("akamai-access-token", "When using the Akamai provider, specify the access token (required when --provider=akamai and edgerc-path not specified)").Default(defaultConfig.AkamaiAccessToken).StringVar(&cfg.AkamaiAccessToken)
-	app.Flag("akamai-edgerc-path", "When using the Akamai provider, specify the .edgerc file path. Path must be reachable form invocation environment. (required when --provider=akamai and *-token, secret serviceconsumerdomain not specified)").Default(defaultConfig.AkamaiEdgercPath).StringVar(&cfg.AkamaiEdgercPath)
-	app.Flag("akamai-edgerc-section", "When using the Akamai provider, specify the .edgerc file path (Optional when edgerc-path is specified)").Default(defaultConfig.AkamaiEdgercSection).StringVar(&cfg.AkamaiEdgercSection)
-	app.Flag("oci-config-file", "When using the OCI provider, specify the OCI configuration file (required when --provider=oci").Default(defaultConfig.OCIConfigFile).StringVar(&cfg.OCIConfigFile)
-	app.Flag("oci-compartment-ocid", "When using the OCI provider, specify the OCID of the OCI compartment containing all managed zones and records.  Required when using OCI IAM instance principal authentication.").StringVar(&cfg.OCICompartmentOCID)
-	app.Flag("oci-zone-scope", "When using OCI provider, filter for zones with this scope (optional, options: GLOBAL, PRIVATE). Defaults to GLOBAL, setting to empty value will target both.").Default(defaultConfig.OCIZoneScope).EnumVar(&cfg.OCIZoneScope, "", "GLOBAL", "PRIVATE")
-	app.Flag("oci-auth-instance-principal", "When using the OCI provider, specify whether OCI IAM instance principal authentication should be used (instead of key-based auth via the OCI config file).").Default(strconv.FormatBool(defaultConfig.OCIAuthInstancePrincipal)).BoolVar(&cfg.OCIAuthInstancePrincipal)
-	app.Flag("oci-zones-cache-duration", "When using the OCI provider, set the zones list cache TTL (0s to disable).").Default(defaultConfig.OCIZoneCacheDuration.String()).DurationVar(&cfg.OCIZoneCacheDuration)
-	app.Flag("inmemory-zone", "Provide a list of pre-configured zones for the inmemory provider; specify multiple times for multiple zones (optional)").Default("").StringsVar(&cfg.InMemoryZones)
-	app.Flag("ovh-endpoint", "When using the OVH provider, specify the endpoint (default: ovh-eu)").Default(defaultConfig.OVHEndpoint).StringVar(&cfg.OVHEndpoint)
-	app.Flag("ovh-api-rate-limit", "When using the OVH provider, specify the API request rate limit, X operations by seconds (default: 20)").Default(strconv.Itoa(defaultConfig.OVHApiRateLimit)).IntVar(&cfg.OVHApiRateLimit)
-	app.Flag("ovh-enable-cname-relative", "When using the OVH provider, specify if CNAME should be treated as relative on target without final dot (default: false)").Default(strconv.FormatBool(defaultConfig.OVHEnableCNAMERelative)).BoolVar(&cfg.OVHEnableCNAMERelative)
-	app.Flag("pdns-server", "When using the PowerDNS/PDNS provider, specify the URL to the pdns server (required when --provider=pdns)").Default(defaultConfig.PDNSServer).StringVar(&cfg.PDNSServer)
-	app.Flag("pdns-server-id", "When using the PowerDNS/PDNS provider, specify the id of the server to retrieve. Should be `localhost` except when the server is behind a proxy (optional when --provider=pdns) (default: localhost)").Default(defaultConfig.PDNSServerID).StringVar(&cfg.PDNSServerID)
-	app.Flag("pdns-api-key", "When using the PowerDNS/PDNS provider, specify the API key to use to authorize requests (required when --provider=pdns)").Default(defaultConfig.PDNSAPIKey).StringVar(&cfg.PDNSAPIKey)
-	app.Flag("pdns-skip-tls-verify", "When using the PowerDNS/PDNS provider, disable verification of any TLS certificates (optional when --provider=pdns) (default: false)").Default(strconv.FormatBool(defaultConfig.PDNSSkipTLSVerify)).BoolVar(&cfg.PDNSSkipTLSVerify)
-	app.Flag("ns1-endpoint", "When using the NS1 provider, specify the URL of the API endpoint to target (default: https://api.nsone.net/v1/)").Default(defaultConfig.NS1Endpoint).StringVar(&cfg.NS1Endpoint)
-	app.Flag("ns1-ignoressl", "When using the NS1 provider, specify whether to verify the SSL certificate (default: false)").Default(strconv.FormatBool(defaultConfig.NS1IgnoreSSL)).BoolVar(&cfg.NS1IgnoreSSL)
-	app.Flag("ns1-min-ttl", "Minimal TTL (in seconds) for records. This value will be used if the provided TTL for a service/ingress is lower than this.").IntVar(&cfg.NS1MinTTLSeconds)
-	app.Flag("digitalocean-api-page-size", "Configure the page size used when querying the DigitalOcean API.").Default(strconv.Itoa(defaultConfig.DigitalOceanAPIPageSize)).IntVar(&cfg.DigitalOceanAPIPageSize)
+	if _, ok := b.(*CobraBinder); ok {
+		providerHelp := "The DNS provider where the DNS records will be created (required, options: " + strings.Join(providerNames, ", ") + ")"
+		b.StringVar("provider", providerHelp, cfg.Provider, &cfg.Provider)
+
+		sourceHelp := "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: " + strings.Join(allowedSources, ", ") + ")"
+		b.StringsVar("source", sourceHelp, append([]string(nil), cfg.Sources...), &cfg.Sources)
+	}
+	b.DurationVar("provider-cache-time", "The time to cache the DNS provider record list requests.", defaultConfig.ProviderCacheTime, &cfg.ProviderCacheTime)
+	b.StringsVar("domain-filter", "Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional)", []string{""}, &cfg.DomainFilter)
+	b.StringsVar("exclude-domains", "Exclude subdomains (optional)", []string{""}, &cfg.ExcludeDomains)
+	b.RegexpVar("regex-domain-filter", "Limit possible domains and target zones by a Regex filter; Overrides domain-filter (optional)", defaultConfig.RegexDomainFilter, &cfg.RegexDomainFilter)
+	b.RegexpVar("regex-domain-exclusion", "Regex filter that excludes domains and target zones matched by regex-domain-filter (optional); Require 'regex-domain-filter' ", defaultConfig.RegexDomainExclusion, &cfg.RegexDomainExclusion)
+	b.StringsVar("zone-name-filter", "Filter target zones by zone domain (For now, only AzureDNS provider is using this flag); specify multiple times for multiple zones (optional)", []string{""}, &cfg.ZoneNameFilter)
+	b.StringsVar("zone-id-filter", "Filter target zones by hosted zone id; specify multiple times for multiple zones (optional)", []string{""}, &cfg.ZoneIDFilter)
+	b.StringVar("google-project", "When using the Google provider, current project is auto-detected, when running on GCP. Specify other project with this. Must be specified when running outside GCP.", defaultConfig.GoogleProject, &cfg.GoogleProject)
+	b.IntVar("google-batch-change-size", "When using the Google provider, set the maximum number of changes that will be applied in each batch.", defaultConfig.GoogleBatchChangeSize, &cfg.GoogleBatchChangeSize)
+	b.DurationVar("google-batch-change-interval", "When using the Google provider, set the interval between batch changes.", defaultConfig.GoogleBatchChangeInterval, &cfg.GoogleBatchChangeInterval)
+	b.EnumVar("google-zone-visibility", "When using the Google provider, filter for zones with this visibility (optional, options: public, private)", defaultConfig.GoogleZoneVisibility, &cfg.GoogleZoneVisibility, "", "public", "private")
+	b.StringVar("alibaba-cloud-config-file", "When using the Alibaba Cloud provider, specify the Alibaba Cloud configuration file (required when --provider=alibabacloud)", defaultConfig.AlibabaCloudConfigFile, &cfg.AlibabaCloudConfigFile)
+	b.EnumVar("alibaba-cloud-zone-type", "When using the Alibaba Cloud provider, filter for zones of this type (optional, options: public, private)", defaultConfig.AlibabaCloudZoneType, &cfg.AlibabaCloudZoneType, "", "public", "private")
+	b.EnumVar("aws-zone-type", "When using the AWS provider, filter for zones of this type (optional, default: any, options: public, private)", defaultConfig.AWSZoneType, &cfg.AWSZoneType, "", "public", "private")
+	b.StringsVar("aws-zone-tags", "When using the AWS provider, filter for zones with these tags", []string{""}, &cfg.AWSZoneTagFilter)
+	b.StringsVar("aws-profile", "When using the AWS provider, name of the profile to use", []string{""}, &cfg.AWSProfiles)
+	b.StringVar("aws-assume-role", "When using the AWS API, assume this IAM role. Useful for hosted zones in another AWS account. Specify the full ARN, e.g. `arn:aws:iam::123455567:role/external-dns` (optional)", defaultConfig.AWSAssumeRole, &cfg.AWSAssumeRole)
+	b.StringVar("aws-assume-role-external-id", "When using the AWS API and assuming a role then specify this external ID` (optional)", defaultConfig.AWSAssumeRoleExternalID, &cfg.AWSAssumeRoleExternalID)
+	b.IntVar("aws-batch-change-size", "When using the AWS provider, set the maximum number of changes that will be applied in each batch.", defaultConfig.AWSBatchChangeSize, &cfg.AWSBatchChangeSize)
+	b.IntVar("aws-batch-change-size-bytes", "When using the AWS provider, set the maximum byte size that will be applied in each batch.", defaultConfig.AWSBatchChangeSizeBytes, &cfg.AWSBatchChangeSizeBytes)
+	b.IntVar("aws-batch-change-size-values", "When using the AWS provider, set the maximum total record values that will be applied in each batch.", defaultConfig.AWSBatchChangeSizeValues, &cfg.AWSBatchChangeSizeValues)
+	b.DurationVar("aws-batch-change-interval", "When using the AWS provider, set the interval between batch changes.", defaultConfig.AWSBatchChangeInterval, &cfg.AWSBatchChangeInterval)
+	b.BoolVar("aws-evaluate-target-health", "When using the AWS provider, set whether to evaluate the health of a DNS target (default: enabled, disable with --no-aws-evaluate-target-health)", defaultConfig.AWSEvaluateTargetHealth, &cfg.AWSEvaluateTargetHealth)
+	b.IntVar("aws-api-retries", "When using the AWS API, set the maximum number of retries before giving up.", defaultConfig.AWSAPIRetries, &cfg.AWSAPIRetries)
+	b.BoolVar("aws-prefer-cname", "When using the AWS provider, prefer using CNAME instead of ALIAS (default: disabled)", defaultConfig.AWSPreferCNAME, &cfg.AWSPreferCNAME)
+	b.DurationVar("aws-zones-cache-duration", "When using the AWS provider, set the zones list cache TTL (0s to disable).", defaultConfig.AWSZoneCacheDuration, &cfg.AWSZoneCacheDuration)
+	b.BoolVar("aws-zone-match-parent", "Expand limit possible target by sub-domains (default: disabled)", defaultConfig.AWSZoneMatchParent, &cfg.AWSZoneMatchParent)
+	b.BoolVar("aws-sd-service-cleanup", "When using the AWS CloudMap provider, delete empty Services without endpoints (default: disabled)", defaultConfig.AWSSDServiceCleanup, &cfg.AWSSDServiceCleanup)
+	b.StringMapVar("aws-sd-create-tag", "When using the AWS CloudMap provider, add tag to created services. The flag can be used multiple times", &cfg.AWSSDCreateTag)
+	b.StringVar("azure-config-file", "When using the Azure provider, specify the Azure configuration file (required when --provider=azure)", defaultConfig.AzureConfigFile, &cfg.AzureConfigFile)
+	b.StringVar("azure-resource-group", "When using the Azure provider, override the Azure resource group to use (optional)", defaultConfig.AzureResourceGroup, &cfg.AzureResourceGroup)
+	b.StringVar("azure-subscription-id", "When using the Azure provider, override the Azure subscription to use (optional)", defaultConfig.AzureSubscriptionID, &cfg.AzureSubscriptionID)
+	b.StringVar("azure-user-assigned-identity-client-id", "When using the Azure provider, override the client id of user assigned identity in config file (optional)", "", &cfg.AzureUserAssignedIdentityClientID)
+	b.DurationVar("azure-zones-cache-duration", "When using the Azure provider, set the zones list cache TTL (0s to disable).", defaultConfig.AzureZonesCacheDuration, &cfg.AzureZonesCacheDuration)
+	b.IntVar("azure-maxretries-count", "When using the Azure provider, set the number of retries for API calls (When less than 0, it disables retries). (optional)", defaultConfig.AzureMaxRetriesCount, &cfg.AzureMaxRetriesCount)
+
+	b.BoolVar("cloudflare-proxied", "When using the Cloudflare provider, specify if the proxy mode must be enabled (default: disabled)", false, &cfg.CloudflareProxied)
+	b.BoolVar("cloudflare-custom-hostnames", "When using the Cloudflare provider, specify if the Custom Hostnames feature will be used. Requires \"Cloudflare for SaaS\" enabled. (default: disabled)", false, &cfg.CloudflareCustomHostnames)
+	b.EnumVar("cloudflare-custom-hostnames-min-tls-version", "When using the Cloudflare provider with the Custom Hostnames, specify which Minimum TLS Version will be used by default. (default: 1.0, options: 1.0, 1.1, 1.2, 1.3)", "1.0", &cfg.CloudflareCustomHostnamesMinTLSVersion, "1.0", "1.1", "1.2", "1.3")
+	b.EnumVar("cloudflare-custom-hostnames-certificate-authority", "When using the Cloudflare provider with the Custom Hostnames, specify which Certificate Authority will be used. A value of none indicates no Certificate Authority will be sent to the Cloudflare API (default: none, options: google, ssl_com, lets_encrypt, none)", "none", &cfg.CloudflareCustomHostnamesCertificateAuthority, "google", "ssl_com", "lets_encrypt", "none")
+	b.IntVar("cloudflare-dns-records-per-page", "When using the Cloudflare provider, specify how many DNS records listed per page, max possible 5,000 (default: 100)", defaultConfig.CloudflareDNSRecordsPerPage, &cfg.CloudflareDNSRecordsPerPage)
+	b.BoolVar("cloudflare-regional-services", "When using the Cloudflare provider, specify if Regional Services feature will be used (default: disabled)", defaultConfig.CloudflareRegionalServices, &cfg.CloudflareRegionalServices)
+	b.StringVar("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)", "", &cfg.CloudflareRegionKey)
+	b.StringVar("cloudflare-record-comment", "When using the Cloudflare provider, specify the comment for the DNS records (default: '')", "", &cfg.CloudflareDNSRecordsComment)
+
+	b.StringVar("coredns-prefix", "When using the CoreDNS provider, specify the prefix name", defaultConfig.CoreDNSPrefix, &cfg.CoreDNSPrefix)
+	b.StringVar("akamai-serviceconsumerdomain", "When using the Akamai provider, specify the base URL (required when --provider=akamai and edgerc-path not specified)", defaultConfig.AkamaiServiceConsumerDomain, &cfg.AkamaiServiceConsumerDomain)
+	b.StringVar("akamai-client-token", "When using the Akamai provider, specify the client token (required when --provider=akamai and edgerc-path not specified)", defaultConfig.AkamaiClientToken, &cfg.AkamaiClientToken)
+	b.StringVar("akamai-client-secret", "When using the Akamai provider, specify the client secret (required when --provider=akamai and edgerc-path not specified)", defaultConfig.AkamaiClientSecret, &cfg.AkamaiClientSecret)
+	b.StringVar("akamai-access-token", "When using the Akamai provider, specify the access token (required when --provider=akamai and edgerc-path not specified)", defaultConfig.AkamaiAccessToken, &cfg.AkamaiAccessToken)
+	b.StringVar("akamai-edgerc-path", "When using the Akamai provider, specify the .edgerc file path. Path must be reachable form invocation environment. (required when --provider=akamai and *-token, secret serviceconsumerdomain not specified)", defaultConfig.AkamaiEdgercPath, &cfg.AkamaiEdgercPath)
+	b.StringVar("akamai-edgerc-section", "When using the Akamai provider, specify the .edgerc file path (Optional when edgerc-path is specified)", defaultConfig.AkamaiEdgercSection, &cfg.AkamaiEdgercSection)
+	b.StringVar("oci-config-file", "When using the OCI provider, specify the OCI configuration file (required when --provider=oci", defaultConfig.OCIConfigFile, &cfg.OCIConfigFile)
+	b.StringVar("oci-compartment-ocid", "When using the OCI provider, specify the OCID of the OCI compartment containing all managed zones and records.  Required when using OCI IAM instance principal authentication.", defaultConfig.OCICompartmentOCID, &cfg.OCICompartmentOCID)
+	b.EnumVar("oci-zone-scope", "When using OCI provider, filter for zones with this scope (optional, options: GLOBAL, PRIVATE). Defaults to GLOBAL, setting to empty value will target both.", defaultConfig.OCIZoneScope, &cfg.OCIZoneScope, "", "GLOBAL", "PRIVATE")
+	b.BoolVar("oci-auth-instance-principal", "When using the OCI provider, specify whether OCI IAM instance principal authentication should be used (instead of key-based auth via the OCI config file).", defaultConfig.OCIAuthInstancePrincipal, &cfg.OCIAuthInstancePrincipal)
+	b.DurationVar("oci-zones-cache-duration", "When using the OCI provider, set the zones list cache TTL (0s to disable).", defaultConfig.OCIZoneCacheDuration, &cfg.OCIZoneCacheDuration)
+	b.StringsVar("inmemory-zone", "Provide a list of pre-configured zones for the inmemory provider; specify multiple times for multiple zones (optional)", []string{""}, &cfg.InMemoryZones)
+	b.StringVar("ovh-endpoint", "When using the OVH provider, specify the endpoint (default: ovh-eu)", defaultConfig.OVHEndpoint, &cfg.OVHEndpoint)
+	b.IntVar("ovh-api-rate-limit", "When using the OVH provider, specify the API request rate limit, X operations by seconds (default: 20)", defaultConfig.OVHApiRateLimit, &cfg.OVHApiRateLimit)
+	b.BoolVar("ovh-enable-cname-relative", "When using the OVH provider, specify if CNAME should be treated as relative on target without final dot (default: false)", defaultConfig.OVHEnableCNAMERelative, &cfg.OVHEnableCNAMERelative)
+	b.StringVar("pdns-server", "When using the PowerDNS/PDNS provider, specify the URL to the pdns server (required when --provider=pdns)", defaultConfig.PDNSServer, &cfg.PDNSServer)
+	b.StringVar("pdns-server-id", "When using the PowerDNS/PDNS provider, specify the id of the server to retrieve. Should be `localhost` except when the server is behind a proxy (optional when --provider=pdns) (default: localhost)", defaultConfig.PDNSServerID, &cfg.PDNSServerID)
+	b.StringVar("pdns-api-key", "When using the PowerDNS/PDNS provider, specify the API key to use to authorize requests (required when --provider=pdns)", defaultConfig.PDNSAPIKey, &cfg.PDNSAPIKey)
+	b.BoolVar("pdns-skip-tls-verify", "When using the PowerDNS/PDNS provider, disable verification of any TLS certificates (optional when --provider=pdns) (default: false)", defaultConfig.PDNSSkipTLSVerify, &cfg.PDNSSkipTLSVerify)
+	b.StringVar("ns1-endpoint", "When using the NS1 provider, specify the URL of the API endpoint to target (default: https://api.nsone.net/v1/)", defaultConfig.NS1Endpoint, &cfg.NS1Endpoint)
+	b.BoolVar("ns1-ignoressl", "When using the NS1 provider, specify whether to verify the SSL certificate (default: false)", defaultConfig.NS1IgnoreSSL, &cfg.NS1IgnoreSSL)
+	b.IntVar("ns1-min-ttl", "Minimal TTL (in seconds) for records. This value will be used if the provided TTL for a service/ingress is lower than this.", cfg.NS1MinTTLSeconds, &cfg.NS1MinTTLSeconds)
+	b.IntVar("digitalocean-api-page-size", "Configure the page size used when querying the DigitalOcean API.", defaultConfig.DigitalOceanAPIPageSize, &cfg.DigitalOceanAPIPageSize)
 	// GoDaddy flags
-	app.Flag("godaddy-api-key", "When using the GoDaddy provider, specify the API Key (required when --provider=godaddy)").Default(defaultConfig.GoDaddyAPIKey).StringVar(&cfg.GoDaddyAPIKey)
-	app.Flag("godaddy-api-secret", "When using the GoDaddy provider, specify the API secret (required when --provider=godaddy)").Default(defaultConfig.GoDaddySecretKey).StringVar(&cfg.GoDaddySecretKey)
-	app.Flag("godaddy-api-ttl", "TTL (in seconds) for records. This value will be used if the provided TTL for a service/ingress is not provided.").Int64Var(&cfg.GoDaddyTTL)
-	app.Flag("godaddy-api-ote", "When using the GoDaddy provider, use OTE api (optional, default: false, when --provider=godaddy)").BoolVar(&cfg.GoDaddyOTE)
+	b.StringVar("godaddy-api-key", "When using the GoDaddy provider, specify the API Key (required when --provider=godaddy)", defaultConfig.GoDaddyAPIKey, &cfg.GoDaddyAPIKey)
+	b.StringVar("godaddy-api-secret", "When using the GoDaddy provider, specify the API secret (required when --provider=godaddy)", defaultConfig.GoDaddySecretKey, &cfg.GoDaddySecretKey)
+	b.Int64Var("godaddy-api-ttl", "TTL (in seconds) for records. This value will be used if the provided TTL for a service/ingress is not provided.", cfg.GoDaddyTTL, &cfg.GoDaddyTTL)
+	b.BoolVar("godaddy-api-ote", "When using the GoDaddy provider, use OTE api (optional, default: false, when --provider=godaddy)", defaultConfig.GoDaddyOTE, &cfg.GoDaddyOTE)
 
 	// Flags related to TLS communication
-	app.Flag("tls-ca", "When using TLS communication, the path to the certificate authority to verify server communications (optionally specify --tls-client-cert for two-way TLS)").Default(defaultConfig.TLSCA).StringVar(&cfg.TLSCA)
-	app.Flag("tls-client-cert", "When using TLS communication, the path to the certificate to present as a client (not required for TLS)").Default(defaultConfig.TLSClientCert).StringVar(&cfg.TLSClientCert)
-	app.Flag("tls-client-cert-key", "When using TLS communication, the path to the certificate key to use with the client certificate (not required for TLS)").Default(defaultConfig.TLSClientCertKey).StringVar(&cfg.TLSClientCertKey)
+	b.StringVar("tls-ca", "When using TLS communication, the path to the certificate authority to verify server communications (optionally specify --tls-client-cert for two-way TLS)", defaultConfig.TLSCA, &cfg.TLSCA)
+	b.StringVar("tls-client-cert", "When using TLS communication, the path to the certificate to present as a client (not required for TLS)", defaultConfig.TLSClientCert, &cfg.TLSClientCert)
+	b.StringVar("tls-client-cert-key", "When using TLS communication, the path to the certificate key to use with the client certificate (not required for TLS)", defaultConfig.TLSClientCertKey, &cfg.TLSClientCertKey)
 
 	// Flags related to Exoscale provider
-	app.Flag("exoscale-apienv", "When using Exoscale provider, specify the API environment (optional)").Default(defaultConfig.ExoscaleAPIEnvironment).StringVar(&cfg.ExoscaleAPIEnvironment)
-	app.Flag("exoscale-apizone", "When using Exoscale provider, specify the API Zone (optional)").Default(defaultConfig.ExoscaleAPIZone).StringVar(&cfg.ExoscaleAPIZone)
-	app.Flag("exoscale-apikey", "Provide your API Key for the Exoscale provider").Default(defaultConfig.ExoscaleAPIKey).StringVar(&cfg.ExoscaleAPIKey)
-	app.Flag("exoscale-apisecret", "Provide your API Secret for the Exoscale provider").Default(defaultConfig.ExoscaleAPISecret).StringVar(&cfg.ExoscaleAPISecret)
+	b.StringVar("exoscale-apienv", "When using Exoscale provider, specify the API environment (optional)", defaultConfig.ExoscaleAPIEnvironment, &cfg.ExoscaleAPIEnvironment)
+	b.StringVar("exoscale-apizone", "When using Exoscale provider, specify the API Zone (optional)", defaultConfig.ExoscaleAPIZone, &cfg.ExoscaleAPIZone)
+	b.StringVar("exoscale-apikey", "Provide your API Key for the Exoscale provider", defaultConfig.ExoscaleAPIKey, &cfg.ExoscaleAPIKey)
+	b.StringVar("exoscale-apisecret", "Provide your API Secret for the Exoscale provider", defaultConfig.ExoscaleAPISecret, &cfg.ExoscaleAPISecret)
 
 	// Flags related to RFC2136 provider
-	app.Flag("rfc2136-host", "When using the RFC2136 provider, specify the host of the DNS server (optionally specify multiple times when using --rfc2136-load-balancing-strategy)").Default(defaultConfig.RFC2136Host[0]).StringsVar(&cfg.RFC2136Host)
-	app.Flag("rfc2136-port", "When using the RFC2136 provider, specify the port of the DNS server").Default(strconv.Itoa(defaultConfig.RFC2136Port)).IntVar(&cfg.RFC2136Port)
-	app.Flag("rfc2136-zone", "When using the RFC2136 provider, specify zone entry of the DNS server to use (can be specified multiple times)").StringsVar(&cfg.RFC2136Zone)
-	app.Flag("rfc2136-create-ptr", "When using the RFC2136 provider, enable PTR management").Default(strconv.FormatBool(defaultConfig.RFC2136CreatePTR)).BoolVar(&cfg.RFC2136CreatePTR)
-	app.Flag("rfc2136-insecure", "When using the RFC2136 provider, specify whether to attach TSIG or not (default: false, requires --rfc2136-tsig-keyname and rfc2136-tsig-secret)").Default(strconv.FormatBool(defaultConfig.RFC2136Insecure)).BoolVar(&cfg.RFC2136Insecure)
-	app.Flag("rfc2136-tsig-keyname", "When using the RFC2136 provider, specify the TSIG key to attached to DNS messages (required when --rfc2136-insecure=false)").Default(defaultConfig.RFC2136TSIGKeyName).StringVar(&cfg.RFC2136TSIGKeyName)
-	app.Flag("rfc2136-tsig-secret", "When using the RFC2136 provider, specify the TSIG (base64) value to attached to DNS messages (required when --rfc2136-insecure=false)").Default(defaultConfig.RFC2136TSIGSecret).StringVar(&cfg.RFC2136TSIGSecret)
-	app.Flag("rfc2136-tsig-secret-alg", "When using the RFC2136 provider, specify the TSIG (base64) value to attached to DNS messages (required when --rfc2136-insecure=false)").Default(defaultConfig.RFC2136TSIGSecretAlg).StringVar(&cfg.RFC2136TSIGSecretAlg)
-	app.Flag("rfc2136-tsig-axfr", "When using the RFC2136 provider, specify the TSIG (base64) value to attached to DNS messages (required when --rfc2136-insecure=false)").BoolVar(&cfg.RFC2136TAXFR)
-	app.Flag("rfc2136-min-ttl", "When using the RFC2136 provider, specify minimal TTL (in duration format) for records. This value will be used if the provided TTL for a service/ingress is lower than this").Default(defaultConfig.RFC2136MinTTL.String()).DurationVar(&cfg.RFC2136MinTTL)
-	app.Flag("rfc2136-gss-tsig", "When using the RFC2136 provider, specify whether to use secure updates with GSS-TSIG using Kerberos (default: false, requires --rfc2136-kerberos-realm, --rfc2136-kerberos-username, and rfc2136-kerberos-password)").Default(strconv.FormatBool(defaultConfig.RFC2136GSSTSIG)).BoolVar(&cfg.RFC2136GSSTSIG)
-	app.Flag("rfc2136-kerberos-username", "When using the RFC2136 provider with GSS-TSIG, specify the username of the user with permissions to update DNS records (required when --rfc2136-gss-tsig=true)").Default(defaultConfig.RFC2136KerberosUsername).StringVar(&cfg.RFC2136KerberosUsername)
-	app.Flag("rfc2136-kerberos-password", "When using the RFC2136 provider with GSS-TSIG, specify the password of the user with permissions to update DNS records (required when --rfc2136-gss-tsig=true)").Default(defaultConfig.RFC2136KerberosPassword).StringVar(&cfg.RFC2136KerberosPassword)
-	app.Flag("rfc2136-kerberos-realm", "When using the RFC2136 provider with GSS-TSIG, specify the realm of the user with permissions to update DNS records (required when --rfc2136-gss-tsig=true)").Default(defaultConfig.RFC2136KerberosRealm).StringVar(&cfg.RFC2136KerberosRealm)
-	app.Flag("rfc2136-batch-change-size", "When using the RFC2136 provider, set the maximum number of changes that will be applied in each batch.").Default(strconv.Itoa(defaultConfig.RFC2136BatchChangeSize)).IntVar(&cfg.RFC2136BatchChangeSize)
-	app.Flag("rfc2136-use-tls", "When using the RFC2136 provider, communicate with name server over tls").BoolVar(&cfg.RFC2136UseTLS)
-	app.Flag("rfc2136-skip-tls-verify", "When using TLS with the RFC2136 provider, disable verification of any TLS certificates").BoolVar(&cfg.RFC2136SkipTLSVerify)
-	app.Flag("rfc2136-load-balancing-strategy", "When using the RFC2136 provider, specify the load balancing strategy (default: disabled, options: random, round-robin, disabled)").Default(defaultConfig.RFC2136LoadBalancingStrategy).EnumVar(&cfg.RFC2136LoadBalancingStrategy, "random", "round-robin", "disabled")
+	b.StringsVar("rfc2136-host", "When using the RFC2136 provider, specify the host of the DNS server (optionally specify multiple times when using --rfc2136-load-balancing-strategy)", []string{defaultConfig.RFC2136Host[0]}, &cfg.RFC2136Host)
+	b.IntVar("rfc2136-port", "When using the RFC2136 provider, specify the port of the DNS server", defaultConfig.RFC2136Port, &cfg.RFC2136Port)
+	b.StringsVar("rfc2136-zone", "When using the RFC2136 provider, specify zone entry of the DNS server to use (can be specified multiple times)", nil, &cfg.RFC2136Zone)
+	b.BoolVar("rfc2136-create-ptr", "When using the RFC2136 provider, enable PTR management", defaultConfig.RFC2136CreatePTR, &cfg.RFC2136CreatePTR)
+	b.BoolVar("rfc2136-insecure", "When using the RFC2136 provider, specify whether to attach TSIG or not (default: false, requires --rfc2136-tsig-keyname and rfc2136-tsig-secret)", defaultConfig.RFC2136Insecure, &cfg.RFC2136Insecure)
+	b.StringVar("rfc2136-tsig-keyname", "When using the RFC2136 provider, specify the TSIG key to attached to DNS messages (required when --rfc2136-insecure=false)", defaultConfig.RFC2136TSIGKeyName, &cfg.RFC2136TSIGKeyName)
+	b.StringVar("rfc2136-tsig-secret", "When using the RFC2136 provider, specify the TSIG (base64) value to attached to DNS messages (required when --rfc2136-insecure=false)", defaultConfig.RFC2136TSIGSecret, &cfg.RFC2136TSIGSecret)
+	b.StringVar("rfc2136-tsig-secret-alg", "When using the RFC2136 provider, specify the TSIG (base64) value to attached to DNS messages (required when --rfc2136-insecure=false)", defaultConfig.RFC2136TSIGSecretAlg, &cfg.RFC2136TSIGSecretAlg)
+	b.BoolVar("rfc2136-tsig-axfr", "When using the RFC2136 provider, specify the TSIG (base64) value to attached to DNS messages (required when --rfc2136-insecure=false)", false, &cfg.RFC2136TAXFR)
+	b.DurationVar("rfc2136-min-ttl", "When using the RFC2136 provider, specify minimal TTL (in duration format) for records. This value will be used if the provided TTL for a service/ingress is lower than this", defaultConfig.RFC2136MinTTL, &cfg.RFC2136MinTTL)
+	b.BoolVar("rfc2136-gss-tsig", "When using the RFC2136 provider, specify whether to use secure updates with GSS-TSIG using Kerberos (default: false, requires --rfc2136-kerberos-realm, --rfc2136-kerberos-username, and rfc2136-kerberos-password)", defaultConfig.RFC2136GSSTSIG, &cfg.RFC2136GSSTSIG)
+	b.StringVar("rfc2136-kerberos-username", "When using the RFC2136 provider with GSS-TSIG, specify the username of the user with permissions to update DNS records (required when --rfc2136-gss-tsig=true)", defaultConfig.RFC2136KerberosUsername, &cfg.RFC2136KerberosUsername)
+	b.StringVar("rfc2136-kerberos-password", "When using the RFC2136 provider with GSS-TSIG, specify the password of the user with permissions to update DNS records (required when --rfc2136-gss-tsig=true)", defaultConfig.RFC2136KerberosPassword, &cfg.RFC2136KerberosPassword)
+	b.StringVar("rfc2136-kerberos-realm", "When using the RFC2136 provider with GSS-TSIG, specify the realm of the user with permissions to update DNS records (required when --rfc2136-gss-tsig=true)", defaultConfig.RFC2136KerberosRealm, &cfg.RFC2136KerberosRealm)
+	b.IntVar("rfc2136-batch-change-size", "When using the RFC2136 provider, set the maximum number of changes that will be applied in each batch.", defaultConfig.RFC2136BatchChangeSize, &cfg.RFC2136BatchChangeSize)
+	b.BoolVar("rfc2136-use-tls", "When using the RFC2136 provider, communicate with name server over tls", defaultConfig.RFC2136UseTLS, &cfg.RFC2136UseTLS)
+	b.BoolVar("rfc2136-skip-tls-verify", "When using TLS with the RFC2136 provider, disable verification of any TLS certificates", defaultConfig.RFC2136SkipTLSVerify, &cfg.RFC2136SkipTLSVerify)
+	b.EnumVar("rfc2136-load-balancing-strategy", "When using the RFC2136 provider, specify the load balancing strategy (default: disabled, options: random, round-robin, disabled)", defaultConfig.RFC2136LoadBalancingStrategy, &cfg.RFC2136LoadBalancingStrategy, "random", "round-robin", "disabled")
 
 	// Flags related to TransIP provider
-	app.Flag("transip-account", "When using the TransIP provider, specify the account name (required when --provider=transip)").Default(defaultConfig.TransIPAccountName).StringVar(&cfg.TransIPAccountName)
-	app.Flag("transip-keyfile", "When using the TransIP provider, specify the path to the private key file (required when --provider=transip)").Default(defaultConfig.TransIPPrivateKeyFile).StringVar(&cfg.TransIPPrivateKeyFile)
+	b.StringVar("transip-account", "When using the TransIP provider, specify the account name (required when --provider=transip)", defaultConfig.TransIPAccountName, &cfg.TransIPAccountName)
+	b.StringVar("transip-keyfile", "When using the TransIP provider, specify the path to the private key file (required when --provider=transip)", defaultConfig.TransIPPrivateKeyFile, &cfg.TransIPPrivateKeyFile)
 
 	// Flags related to Pihole provider
-	app.Flag("pihole-server", "When using the Pihole provider, the base URL of the Pihole web server (required when --provider=pihole)").Default(defaultConfig.PiholeServer).StringVar(&cfg.PiholeServer)
-	app.Flag("pihole-password", "When using the Pihole provider, the password to the server if it is protected").Default(defaultConfig.PiholePassword).StringVar(&cfg.PiholePassword)
-	app.Flag("pihole-tls-skip-verify", "When using the Pihole provider, disable verification of any TLS certificates").BoolVar(&cfg.PiholeTLSInsecureSkipVerify)
-	app.Flag("pihole-api-version", "When using the Pihole provider, specify the pihole API version (default: 5, options: 5, 6)").Default(defaultConfig.PiholeApiVersion).StringVar(&cfg.PiholeApiVersion)
+	b.StringVar("pihole-server", "When using the Pihole provider, the base URL of the Pihole web server (required when --provider=pihole)", defaultConfig.PiholeServer, &cfg.PiholeServer)
+	b.StringVar("pihole-password", "When using the Pihole provider, the password to the server if it is protected", defaultConfig.PiholePassword, &cfg.PiholePassword)
+	b.BoolVar("pihole-tls-skip-verify", "When using the Pihole provider, disable verification of any TLS certificates", defaultConfig.PiholeTLSInsecureSkipVerify, &cfg.PiholeTLSInsecureSkipVerify)
+	b.StringVar("pihole-api-version", "When using the Pihole provider, specify the pihole API version (default: 5, options: 5, 6)", defaultConfig.PiholeApiVersion, &cfg.PiholeApiVersion)
 
 	// Flags related to the Plural provider
-	app.Flag("plural-cluster", "When using the plural provider, specify the cluster name you're running with").Default(defaultConfig.PluralCluster).StringVar(&cfg.PluralCluster)
-	app.Flag("plural-provider", "When using the plural provider, specify the provider name you're running with").Default(defaultConfig.PluralProvider).StringVar(&cfg.PluralProvider)
+	b.StringVar("plural-cluster", "When using the plural provider, specify the cluster name you're running with", defaultConfig.PluralCluster, &cfg.PluralCluster)
+	b.StringVar("plural-provider", "When using the plural provider, specify the provider name you're running with", defaultConfig.PluralProvider, &cfg.PluralProvider)
 
 	// Flags related to policies
-	app.Flag("policy", "Modify how DNS records are synchronized between sources and providers (default: sync, options: sync, upsert-only, create-only)").Default(defaultConfig.Policy).EnumVar(&cfg.Policy, "sync", "upsert-only", "create-only")
+	b.EnumVar("policy", "Modify how DNS records are synchronized between sources and providers (default: sync, options: sync, upsert-only, create-only)", defaultConfig.Policy, &cfg.Policy, "sync", "upsert-only", "create-only")
 
 	// Flags related to the registry
-	app.Flag("registry", "The registry implementation to use to keep track of DNS record ownership (default: txt, options: txt, noop, dynamodb, aws-sd)").Default(defaultConfig.Registry).EnumVar(&cfg.Registry, "txt", "noop", "dynamodb", "aws-sd")
-	app.Flag("txt-owner-id", "When using the TXT or DynamoDB registry, a name that identifies this instance of ExternalDNS (default: default)").Default(defaultConfig.TXTOwnerID).StringVar(&cfg.TXTOwnerID)
-	app.Flag("txt-prefix", "When using the TXT registry, a custom string that's prefixed to each ownership DNS record (optional). Could contain record type template like '%{record_type}-prefix-'. Mutual exclusive with txt-suffix!").Default(defaultConfig.TXTPrefix).StringVar(&cfg.TXTPrefix)
-	app.Flag("txt-suffix", "When using the TXT registry, a custom string that's suffixed to the host portion of each ownership DNS record (optional). Could contain record type template like '-%{record_type}-suffix'. Mutual exclusive with txt-prefix!").Default(defaultConfig.TXTSuffix).StringVar(&cfg.TXTSuffix)
-	app.Flag("txt-wildcard-replacement", "When using the TXT registry, a custom string that's used instead of an asterisk for TXT records corresponding to wildcard DNS records (optional)").Default(defaultConfig.TXTWildcardReplacement).StringVar(&cfg.TXTWildcardReplacement)
-	app.Flag("txt-encrypt-enabled", "When using the TXT registry, set if TXT records should be encrypted before stored (default: disabled)").BoolVar(&cfg.TXTEncryptEnabled)
-	app.Flag("txt-encrypt-aes-key", "When using the TXT registry, set TXT record decryption and encryption 32 byte aes key (required when --txt-encrypt=true)").Default(defaultConfig.TXTEncryptAESKey).StringVar(&cfg.TXTEncryptAESKey)
-	app.Flag("dynamodb-region", "When using the DynamoDB registry, the AWS region of the DynamoDB table (optional)").Default(cfg.AWSDynamoDBRegion).StringVar(&cfg.AWSDynamoDBRegion)
-	app.Flag("dynamodb-table", "When using the DynamoDB registry, the name of the DynamoDB table (default: \"external-dns\")").Default(defaultConfig.AWSDynamoDBTable).StringVar(&cfg.AWSDynamoDBTable)
+	b.EnumVar("registry", "The registry implementation to use to keep track of DNS record ownership (default: txt, options: txt, noop, dynamodb, aws-sd)", defaultConfig.Registry, &cfg.Registry, "txt", "noop", "dynamodb", "aws-sd")
+	b.StringVar("txt-owner-id", "When using the TXT or DynamoDB registry, a name that identifies this instance of ExternalDNS (default: default)", defaultConfig.TXTOwnerID, &cfg.TXTOwnerID)
+	b.StringVar("txt-prefix", "When using the TXT registry, a custom string that's prefixed to each ownership DNS record (optional). Could contain record type template like '%{record_type}-prefix-'. Mutual exclusive with txt-suffix!", defaultConfig.TXTPrefix, &cfg.TXTPrefix)
+	b.StringVar("txt-suffix", "When using the TXT registry, a custom string that's suffixed to the host portion of each ownership DNS record (optional). Could contain record type template like '-%{record_type}-suffix'. Mutual exclusive with txt-prefix!", defaultConfig.TXTSuffix, &cfg.TXTSuffix)
+	b.StringVar("txt-wildcard-replacement", "When using the TXT registry, a custom string that's used instead of an asterisk for TXT records corresponding to wildcard DNS records (optional)", defaultConfig.TXTWildcardReplacement, &cfg.TXTWildcardReplacement)
+	b.BoolVar("txt-encrypt-enabled", "When using the TXT registry, set if TXT records should be encrypted before stored (default: disabled)", defaultConfig.TXTEncryptEnabled, &cfg.TXTEncryptEnabled)
+	b.StringVar("txt-encrypt-aes-key", "When using the TXT registry, set TXT record decryption and encryption 32 byte aes key (required when --txt-encrypt=true)", defaultConfig.TXTEncryptAESKey, &cfg.TXTEncryptAESKey)
+	b.StringVar("dynamodb-region", "When using the DynamoDB registry, the AWS region of the DynamoDB table (optional)", cfg.AWSDynamoDBRegion, &cfg.AWSDynamoDBRegion)
+	b.StringVar("dynamodb-table", "When using the DynamoDB registry, the name of the DynamoDB table (default: \"external-dns\")", defaultConfig.AWSDynamoDBTable, &cfg.AWSDynamoDBTable)
 
 	// Flags related to the main control loop
-	app.Flag("txt-cache-interval", "The interval between cache synchronizations in duration format (default: disabled)").Default(defaultConfig.TXTCacheInterval.String()).DurationVar(&cfg.TXTCacheInterval)
-	app.Flag("interval", "The interval between two consecutive synchronizations in duration format (default: 1m)").Default(defaultConfig.Interval.String()).DurationVar(&cfg.Interval)
-	app.Flag("min-event-sync-interval", "The minimum interval between two consecutive synchronizations triggered from kubernetes events in duration format (default: 5s)").Default(defaultConfig.MinEventSyncInterval.String()).DurationVar(&cfg.MinEventSyncInterval)
-	app.Flag("once", "When enabled, exits the synchronization loop after the first iteration (default: disabled)").BoolVar(&cfg.Once)
-	app.Flag("dry-run", "When enabled, prints DNS record changes rather than actually performing them (default: disabled)").BoolVar(&cfg.DryRun)
-	app.Flag("events", "When enabled, in addition to running every interval, the reconciliation loop will get triggered when supported sources change (default: disabled)").BoolVar(&cfg.UpdateEvents)
-	app.Flag("min-ttl", "Configure global TTL for records in duration format. This value is used when the TTL for a source is not set or set to 0. (optional; examples: 1m12s, 72s, 72)").DurationVar(&cfg.MinTTL)
+	b.DurationVar("txt-cache-interval", "The interval between cache synchronizations in duration format (default: disabled)", defaultConfig.TXTCacheInterval, &cfg.TXTCacheInterval)
+	b.DurationVar("interval", "The interval between two consecutive synchronizations in duration format (default: 1m)", defaultConfig.Interval, &cfg.Interval)
+	b.DurationVar("min-event-sync-interval", "The minimum interval between two consecutive synchronizations triggered from kubernetes events in duration format (default: 5s)", defaultConfig.MinEventSyncInterval, &cfg.MinEventSyncInterval)
+	b.BoolVar("once", "When enabled, exits the synchronization loop after the first iteration (default: disabled)", defaultConfig.Once, &cfg.Once)
+	b.BoolVar("dry-run", "When enabled, prints DNS record changes rather than actually performing them (default: disabled)", defaultConfig.DryRun, &cfg.DryRun)
+	b.BoolVar("events", "When enabled, in addition to running every interval, the reconciliation loop will get triggered when supported sources change (default: disabled)", defaultConfig.UpdateEvents, &cfg.UpdateEvents)
 
 	// Miscellaneous flags
-	app.Flag("log-format", "The format in which log messages are printed (default: text, options: text, json)").Default(defaultConfig.LogFormat).EnumVar(&cfg.LogFormat, "text", "json")
-	app.Flag("metrics-address", "Specify where to serve the metrics and health check endpoint (default: :7979)").Default(defaultConfig.MetricsAddress).StringVar(&cfg.MetricsAddress)
-	app.Flag("log-level", "Set the level of logging. (default: info, options: panic, debug, info, warning, error, fatal)").Default(defaultConfig.LogLevel).EnumVar(&cfg.LogLevel, allLogLevelsAsStrings()...)
+	b.EnumVar("log-format", "The format in which log messages are printed (default: text, options: text, json)", defaultConfig.LogFormat, &cfg.LogFormat, "text", "json")
+	b.StringVar("metrics-address", "Specify where to serve the metrics and health check endpoint (default: :7979)", defaultConfig.MetricsAddress, &cfg.MetricsAddress)
+	b.EnumVar("log-level", "Set the level of logging. (default: info, options: panic, debug, info, warning, error, fatal)", defaultConfig.LogLevel, &cfg.LogLevel, allLogLevelsAsStrings()...)
 
 	// Webhook provider
-	app.Flag("webhook-provider-url", "The URL of the remote endpoint to call for the webhook provider (default: http://localhost:8888)").Default(defaultConfig.WebhookProviderURL).StringVar(&cfg.WebhookProviderURL)
-	app.Flag("webhook-provider-read-timeout", "The read timeout for the webhook provider in duration format (default: 5s)").Default(defaultConfig.WebhookProviderReadTimeout.String()).DurationVar(&cfg.WebhookProviderReadTimeout)
-	app.Flag("webhook-provider-write-timeout", "The write timeout for the webhook provider in duration format (default: 10s)").Default(defaultConfig.WebhookProviderWriteTimeout.String()).DurationVar(&cfg.WebhookProviderWriteTimeout)
+	b.StringVar("webhook-provider-url", "The URL of the remote endpoint to call for the webhook provider (default: http://localhost:8888)", defaultConfig.WebhookProviderURL, &cfg.WebhookProviderURL)
+	b.DurationVar("webhook-provider-read-timeout", "The read timeout for the webhook provider in duration format (default: 5s)", defaultConfig.WebhookProviderReadTimeout, &cfg.WebhookProviderReadTimeout)
+	b.DurationVar("webhook-provider-write-timeout", "The write timeout for the webhook provider in duration format (default: 10s)", defaultConfig.WebhookProviderWriteTimeout, &cfg.WebhookProviderWriteTimeout)
+	b.BoolVar("webhook-server", "When enabled, runs as a webhook server instead of a controller. (default: false).", defaultConfig.WebhookServer, &cfg.WebhookServer)
+}
 
-	app.Flag("webhook-server", "When enabled, runs as a webhook server instead of a controller. (default: false).").BoolVar(&cfg.WebhookServer)
+func (cfg *Config) AddSourceWrapper(name string) {
+	if cfg.sourceWrappers == nil {
+		cfg.sourceWrappers = make(map[string]bool)
+	}
+	cfg.sourceWrappers[name] = true
+}
+
+// IsSourceWrapperInstrumented returns whether a source wrapper is enabled or not.
+func (cfg *Config) IsSourceWrapperInstrumented(name string) bool {
+	if cfg.sourceWrappers == nil {
+		return false
+	}
+	_, ok := cfg.sourceWrappers[name]
+	return ok
+}
+
+func App(cfg *Config) *kingpin.Application {
+	app := kingpin.New("external-dns", "ExternalDNS synchronizes exposed Kubernetes Services and Ingresses with DNS providers.\n\nNote that all flags may be replaced with env vars - `--flag` -> `EXTERNAL_DNS_FLAG=1` or `--flag value` -> `EXTERNAL_DNS_FLAG=value`")
+	app.Version(Version)
+	app.DefaultEnvars()
+
+	bindFlags(NewKingpinBinder(app), cfg)
+
+	// Kingpin-only semantics: preserve Required/PlaceHolder and enum validation
+	// that Kingpin provided before the flags were migrated into the binder.
+	app.Flag("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)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, providerNames...)
+
+	// Reintroduce source enum/required validation in Kingpin to match previous behavior.
+	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")
 
 	return app
 }
diff --git a/provider/cloudflare/cloudflare.go b/provider/cloudflare/cloudflare.go
index 55de4d8d..c701d8b9 100644
--- a/provider/cloudflare/cloudflare.go
+++ b/provider/cloudflare/cloudflare.go
@@ -738,6 +738,12 @@ func (p *CloudFlareProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) ([]
 
 		p.adjustEndpointProviderSpecificRegionKeyProperty(e)
 
+		if p.DNSRecordsConfig.Comment != "" {
+			if _, found := e.GetProviderSpecificProperty(annotations.CloudflareRecordCommentKey); !found {
+				e.SetProviderSpecificProperty(annotations.CloudflareRecordCommentKey, p.DNSRecordsConfig.Comment)
+			}
+		}
+
 		adjustedEndpoints = append(adjustedEndpoints, e)
 	}
 	return adjustedEndpoints, nil



Test patch:
diff --git a/pkg/apis/externaldns/binders_test.go b/pkg/apis/externaldns/binders_test.go
index 3200a06f..7829cf8d 100644
--- a/pkg/apis/externaldns/binders_test.go
+++ b/pkg/apis/externaldns/binders_test.go
@@ -17,6 +17,8 @@ limitations under the License.
 package externaldns
 
 import (
+	"errors"
+	"regexp"
 	"testing"
 	"time"
 
@@ -26,6 +28,10 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
+type badSetter struct{}
+
+func (b *badSetter) Set(s string) error { return errors.New("bad default") }
+
 func TestKingpinBinderParsesAllTypes(t *testing.T) {
 	app := kingpin.New("test", "")
 	b := NewKingpinBinder(app)
@@ -137,3 +143,139 @@ func TestCobraBinderEnumNotValidatedHere(t *testing.T) {
 	require.NoError(t, err)
 	assert.Equal(t, "c", e)
 }
+
+// Cobra requires --<flag>=false
+func TestCobraBinderNoBooleanNegationFormUnsupported(t *testing.T) {
+	cmd := &cobra.Command{Use: "test"}
+	b := NewCobraBinder(cmd)
+
+	var v bool
+	b.BoolVar("v", "bool flag", true, &v)
+
+	cmd.SetArgs([]string{"--no-v"})
+	err := cmd.Execute()
+	require.Error(t, err)
+}
+
+func TestCobraRegexValueSetStringType(t *testing.T) {
+	var r *regexp.Regexp
+	rv := &regexpValue{target: &r}
+
+	require.Equal(t, "regexp", rv.Type())
+	// empty when target nil
+	assert.Empty(t, rv.String())
+
+	// invalid pattern returns error
+	err := rv.Set("(")
+	require.Error(t, err)
+
+	// valid pattern sets target
+	err = rv.Set("^foo$")
+	require.NoError(t, err)
+	require.NotNil(t, r)
+	assert.Equal(t, "^foo$", r.String())
+	assert.Equal(t, "^foo$", rv.String())
+}
+
+func TestCobraRegexpVarDefaultAndInvalidValue(t *testing.T) {
+	cmd := &cobra.Command{Use: "test"}
+	b := NewCobraBinder(cmd)
+
+	var r *regexp.Regexp
+	// Provide a valid default: should set target immediately
+	b.RegexpVar("re", "help", regexp.MustCompile("^x+$"), &r)
+	require.NotNil(t, r)
+	assert.Equal(t, "^x+$", r.String())
+
+	// Executing with an invalid value should produce an error
+	cmd2 := &cobra.Command{Use: "test2"}
+	b2 := NewCobraBinder(cmd2)
+	var r2 *regexp.Regexp
+	b2.RegexpVar("re", "help", nil, &r2)
+	cmd2.SetArgs([]string{"--re=invalid("})
+	err := cmd2.Execute()
+	require.Error(t, err)
+}
+
+func TestCobraStringMapVarDefaultEmpty(t *testing.T) {
+	cmd := &cobra.Command{Use: "test"}
+	b := NewCobraBinder(cmd)
+
+	var m map[string]string
+	b.StringMapVar("m", "help", &m)
+
+	cmd.SetArgs([]string{})
+	err := cmd.Execute()
+	require.NoError(t, err)
+	require.NotNil(t, m)
+	assert.Empty(t, m)
+}
+
+func TestKingpinRegexpVarDefaultAndParse(t *testing.T) {
+	app := kingpin.New("test", "")
+	b := NewKingpinBinder(app)
+
+	var r *regexp.Regexp
+	b.RegexpVar("re", "help", regexp.MustCompile("^a+$"), &r)
+
+	_, err := app.Parse([]string{})
+	require.NoError(t, err)
+	require.NotNil(t, r)
+	assert.Equal(t, "^a+$", r.String())
+
+	// user-provided value should override default
+	var r2 *regexp.Regexp
+	app2 := kingpin.New("test2", "")
+	b2 := NewKingpinBinder(app2)
+	b2.RegexpVar("re", "help", nil, &r2)
+	_, err = app2.Parse([]string{"--re=^b+$"})
+	require.NoError(t, err)
+	require.NotNil(t, r2)
+	assert.Equal(t, "^b+$", r2.String())
+}
+
+func TestKingpinStringsEnumVarWithAndWithoutDefault(t *testing.T) {
+	app := kingpin.New("test", "")
+	b := NewKingpinBinder(app)
+
+	var vals []string
+	b.StringsEnumVar("se", "help", []string{"a", "b"}, &vals, "a", "b", "c")
+	_, err := app.Parse([]string{})
+	require.NoError(t, err)
+	assert.ElementsMatch(t, []string{"a", "b"}, vals)
+
+	// without default
+	app2 := kingpin.New("test2", "")
+	b2 := NewKingpinBinder(app2)
+	var vals2 []string
+	b2.StringsEnumVar("se", "help", nil, &vals2, "a", "b", "c")
+	_, err = app2.Parse([]string{"--se=a", "--se=c"})
+	require.NoError(t, err)
+	assert.ElementsMatch(t, []string{"a", "c"}, vals2)
+}
+
+func TestCobraStringsEnumVarWithAndWithoutDefault(t *testing.T) {
+	cmd := &cobra.Command{Use: "test"}
+	b := NewCobraBinder(cmd)
+
+	var vals []string
+	b.StringsEnumVar("se", "help", []string{"x", "y"}, &vals, "x", "y")
+	cmd.SetArgs([]string{})
+	require.NoError(t, cmd.Execute())
+	assert.ElementsMatch(t, []string{"x", "y"}, vals)
+
+	// without default
+	cmd2 := &cobra.Command{Use: "test2"}
+	b2 := NewCobraBinder(cmd2)
+	var vals2 []string
+	b2.StringsEnumVar("se", "help", nil, &vals2, "x", "y")
+	cmd2.SetArgs([]string{"--se=a", "--se=b"})
+	require.NoError(t, cmd2.Execute())
+	assert.ElementsMatch(t, []string{"a", "b"}, vals2)
+}
+
+func TestSetRegexDefaultPanicsOnInvalidDefault(t *testing.T) {
+	bs := &badSetter{}
+	def := regexp.MustCompile("^")
+	require.Panics(t, func() { setRegexpDefault(bs, def, "flag") })
+}
diff --git a/pkg/apis/externaldns/types_test.go b/pkg/apis/externaldns/types_test.go
index 0e65e6e1..9a35cd3e 100644
--- a/pkg/apis/externaldns/types_test.go
+++ b/pkg/apis/externaldns/types_test.go
@@ -17,14 +17,19 @@ limitations under the License.
 package externaldns
 
 import (
+	"fmt"
 	"os"
+	"reflect"
 	"regexp"
+	"sort"
 	"testing"
 	"time"
 
 	"sigs.k8s.io/external-dns/endpoint"
 
+	"github.com/alecthomas/kingpin/v2"
 	"github.com/sirupsen/logrus"
+	"github.com/spf13/cobra"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
@@ -519,6 +524,29 @@ func TestParseFlags(t *testing.T) {
 	}
 }
 
+func TestParseFlagsCobraExecuteError(t *testing.T) {
+	cfg := NewConfig()
+	err := cfg.ParseFlags([]string{"--cli-backend=cobra", "--unknown-flag"})
+	require.Error(t, err)
+}
+
+func TestParseFlagsKingpinParseError(t *testing.T) {
+	cfg := NewConfig()
+	err := cfg.ParseFlags([]string{"--unknown-flag"})
+	require.Error(t, err)
+}
+
+func TestConfigStringMasksSecureFields(t *testing.T) {
+	cfg := NewConfig()
+	cfg.AWSAssumeRoleExternalID = "sensitive-value"
+	cfg.GoDaddyAPIKey = "another-secret"
+
+	s := cfg.String()
+	require.NotContains(t, s, "sensitive-value")
+	require.NotContains(t, s, "another-secret")
+	require.Contains(t, s, passwordMask)
+}
+
 // helper functions
 
 func setEnv(t *testing.T, env map[string]string) map[string]string {
@@ -532,8 +560,7 @@ func setEnv(t *testing.T, env map[string]string) map[string]string {
 	return originalEnv
 }
 
-// Additional tests to verify the experimental CLI switch behavior.
-// Default path should use kingpin and parse flags correctly.
+// Default path should use kingpin and parse flags correctly
 func TestParseFlagsDefaultKingpin(t *testing.T) {
 	t.Setenv("EXTERNAL_DNS_CLI", "")
 
@@ -633,14 +660,16 @@ func TestNewCobraCommandDefaultsApplied(t *testing.T) {
 	cfg := NewConfig()
 	// Pre-populate some defaults to confirm they persist without args.
 	cfg.Sources = []string{"service"}
+	// provider to preserve the original test intent.
+	cfg.Provider = "aws"
 
 	cmd := newCobraCommand(cfg)
 	cmd.SetArgs([]string{})
 	require.NoError(t, cmd.Execute())
 
 	assert.ElementsMatch(t, []string{"service"}, cfg.Sources)
-	// DomainFilter uses the package default for this binding.
-	assert.Empty(t, cfg.DomainFilter)
+	// DomainFilter defaults to a single empty entry for historical parity.
+	assert.Equal(t, []string{""}, cfg.DomainFilter)
 }
 
 func TestParseFlagsCliFlagCobraParitySubset(t *testing.T) {
@@ -707,6 +736,22 @@ func TestParseFlagsCliFlagSeparatedValue(t *testing.T) {
 	assert.ElementsMatch(t, []string{"service"}, cfg.Sources)
 }
 
+// Env vars are accpeted by Kingpin backend but ignored by Cobra
+func TestEnvVarsIgnoredByCobraBackend(t *testing.T) {
+	t.Setenv("EXTERNAL_DNS_CLI", "cobra")
+	t.Setenv("EXTERNAL_DNS_NAMESPACE", "ns-from-env")
+	t.Setenv("EXTERNAL_DNS_DRY_RUN", "1")
+
+	cfg := NewConfig()
+	// Only required args for Cobra validation
+	args := []string{"--provider=aws", "--source=service"}
+	require.NoError(t, cfg.ParseFlags(args))
+
+	// Ignore env vars and keep defaults
+	assert.Empty(t, cfg.Namespace)
+	assert.False(t, cfg.DryRun)
+}
+
 func restoreEnv(t *testing.T, originalEnv map[string]string) {
 	for k, v := range originalEnv {
 		require.NoError(t, os.Setenv(k, v))
@@ -724,3 +769,726 @@ func TestPasswordsNotLogged(t *testing.T) {
 	assert.NotContains(t, s, "pdns-api-key")
 	assert.NotContains(t, s, "tsig-secret")
 }
+
+// Additional assertions to cover previously unasserted flags. These focus on
+// exercising Kingpin flag bindings for a wide set of providers/features.
+// parseCfg builds a Config by parsing base flags plus any extras.
+func parseCfg(t *testing.T, extra ...string) *Config {
+	t.Helper()
+	cfg := NewConfig()
+	args := append([]string{"--provider=google", "--source=service"}, extra...)
+	require.NoError(t, cfg.ParseFlags(args))
+	return cfg
+}
+
+func TestParseFlagsAlibabaCloud(t *testing.T) {
+	t.Parallel()
+	cfg := parseCfg(t,
+		"--alibaba-cloud-config-file=/etc/kubernetes/alibaba-override.json",
+		"--alibaba-cloud-zone-type=private",
+	)
+	assert.Equal(t, "/etc/kubernetes/alibaba-override.json", cfg.AlibabaCloudConfigFile)
+	assert.Equal(t, "private", cfg.AlibabaCloudZoneType)
+}
+
+func TestParseFlagsPublishingAndFilters(t *testing.T) {
+	t.Parallel()
+	cfg := parseCfg(t,
+		"--always-publish-not-ready-addresses",
+		"--annotation-filter=key=value",
+		"--combine-fqdn-annotation",
+		"--default-targets=1.2.3.4",
+		"--default-targets=5.6.7.8",
+		"--exclude-record-types=TXT",
+		"--exclude-record-types=CNAME",
+		"--expose-internal-ipv6",
+		"--force-default-targets",
+		"--ingress-class=nginx",
+		"--ingress-class=internal",
+		"--label-filter=environment=prod",
+		"--nat64-networks=64:ff9b::/96",
+		"--nat64-networks=64:ff9b:1::/48",
+		"--publish-host-ip",
+		"--publish-internal-services",
+		"--resolve-service-load-balancer-hostname",
+		"--service-type-filter=ClusterIP",
+		"--service-type-filter=NodePort",
+		"--events-emit=RecordReady",
+		"--events-emit=RecordDeleted",
+	)
+	assert.True(t, cfg.AlwaysPublishNotReadyAddresses)
+	assert.Equal(t, "key=value", cfg.AnnotationFilter)
+	assert.True(t, cfg.CombineFQDNAndAnnotation)
+	assert.ElementsMatch(t, []string{"1.2.3.4", "5.6.7.8"}, cfg.DefaultTargets)
+	assert.ElementsMatch(t, []string{"TXT", "CNAME"}, cfg.ExcludeDNSRecordTypes)
+	assert.True(t, cfg.ExposeInternalIPV6)
+	assert.True(t, cfg.ForceDefaultTargets)
+	assert.ElementsMatch(t, []string{"nginx", "internal"}, cfg.IngressClassNames)
+	assert.Equal(t, "environment=prod", cfg.LabelFilter)
+	assert.ElementsMatch(t, []string{"64:ff9b::/96", "64:ff9b:1::/48"}, cfg.NAT64Networks)
+	assert.True(t, cfg.PublishHostIP)
+	assert.True(t, cfg.PublishInternal)
+	assert.True(t, cfg.ResolveServiceLoadBalancerHostname)
+	assert.ElementsMatch(t, []string{"ClusterIP", "NodePort"}, cfg.ServiceTypeFilter)
+	assert.ElementsMatch(t, []string{"RecordReady", "RecordDeleted"}, cfg.EmitEvents)
+}
+
+func TestParseFlagsGateway(t *testing.T) {
+	t.Parallel()
+	cfg := parseCfg(t,
+		"--gateway-label-filter=app=gateway",
+		"--gateway-name=gw-1",
+		"--gateway-namespace=gw-ns",
+	)
+	assert.Equal(t, "app=gateway", cfg.GatewayLabelFilter)
+	assert.Equal(t, "gw-1", cfg.GatewayName)
+	assert.Equal(t, "gw-ns", cfg.GatewayNamespace)
+}
+
+func TestParseFlagsCloudFoundry(t *testing.T) {
+	t.Parallel()
+	cfg := parseCfg(t,
+		"--cf-api-endpoint=https://api.cf.example",
+		"--cf-username=user1",
+		"--cf-password=p@ss",
+	)
+	assert.Equal(t, "https://api.cf.example", cfg.CFAPIEndpoint)
+	assert.Equal(t, "user1", cfg.CFUsername)
+	assert.Equal(t, "p@ss", cfg.CFPassword)
+}
+
+func TestParseFlagsAzure(t *testing.T) {
+	t.Parallel()
+	cfg := parseCfg(t,
+		"--azure-user-assigned-identity-client-id=00000000-0000-0000-0000-000000000000",
+		"--azure-zones-cache-duration=30s",
+	)
+	assert.Equal(t, "00000000-0000-0000-0000-000000000000", cfg.AzureUserAssignedIdentityClientID)
+	assert.Equal(t, 30*time.Second, cfg.AzureZonesCacheDuration)
+}
+
+func TestParseFlagsCloudflare(t *testing.T) {
+	t.Parallel()
+	cfg := parseCfg(t, "--cloudflare-record-comment=managed-by-external-dns")
+	assert.Equal(t, "managed-by-external-dns", cfg.CloudflareDNSRecordsComment)
+}
+
+func TestParseFlagsNS1(t *testing.T) {
+	t.Parallel()
+	cfg := parseCfg(t, "--ns1-min-ttl=60")
+	assert.Equal(t, 60, cfg.NS1MinTTLSeconds)
+}
+
+func TestParseFlagsOVH(t *testing.T) {
+	t.Parallel()
+	cfg := parseCfg(t, "--ovh-enable-cname-relative")
+	assert.True(t, cfg.OVHEnableCNAMERelative)
+}
+
+func TestParseFlagsPihole(t *testing.T) {
+	t.Parallel()
+	cfg := parseCfg(t,
+		"--pihole-server=https://pi.example",
+		"--pihole-password=pw",
+		"--pihole-tls-skip-verify",
+	)
+	assert.Equal(t, "https://pi.example", cfg.PiholeServer)
+	assert.Equal(t, "pw", cfg.PiholePassword)
+	assert.True(t, cfg.PiholeTLSInsecureSkipVerify)
+}
+
+func TestParseFlagsOCI(t *testing.T) {
+	t.Parallel()
+	cfg := parseCfg(t,
+		"--oci-auth-instance-principal",
+		"--oci-compartment-ocid=ocid1.compartment.oc1..aaaa",
+	)
+	assert.True(t, cfg.OCIAuthInstancePrincipal)
+	assert.Equal(t, "ocid1.compartment.oc1..aaaa", cfg.OCICompartmentOCID)
+}
+
+func TestParseFlagsPlural(t *testing.T) {
+	t.Parallel()
+	cfg := parseCfg(t,
+		"--plural-cluster=mycluster",
+		"--plural-provider=aws",
+	)
+	assert.Equal(t, "mycluster", cfg.PluralCluster)
+	assert.Equal(t, "aws", cfg.PluralProvider)
+}
+
+func TestParseFlagsProviderCacheAndDynamoDB(t *testing.T) {
+	t.Parallel()
+	cfg := parseCfg(t,
+		"--provider-cache-time=20s",
+		"--dynamodb-region=us-east-2",
+	)
+	assert.Equal(t, 20*time.Second, cfg.ProviderCacheTime)
+	assert.Equal(t, "us-east-2", cfg.AWSDynamoDBRegion)
+}
+
+func TestParseFlagsGoDaddy(t *testing.T) {
+	t.Parallel()
+	cfg := parseCfg(t,
+		"--godaddy-api-key=key",
+		"--godaddy-api-secret=secret",
+		"--godaddy-api-ttl=1234",
+		"--godaddy-api-ote",
+	)
+	assert.Equal(t, "key", cfg.GoDaddyAPIKey)
+	assert.Equal(t, "secret", cfg.GoDaddySecretKey)
+	assert.Equal(t, int64(1234), cfg.GoDaddyTTL)
+	assert.True(t, cfg.GoDaddyOTE)
+}
+
+func TestParseFlagsRFC2136(t *testing.T) {
+	t.Parallel()
+	cfg := parseCfg(t,
+		"--rfc2136-port=5353",
+		"--rfc2136-zone=example.org.",
+		"--rfc2136-zone=example.com.",
+		"--rfc2136-create-ptr",
+		"--rfc2136-insecure",
+		"--rfc2136-kerberos-realm=EXAMPLE.COM",
+		"--rfc2136-kerberos-username=svc-externaldns",
+		"--rfc2136-kerberos-password=secret",
+		"--rfc2136-tsig-keyname=keyname.",
+		"--rfc2136-tsig-secret=base64secret",
+		"--rfc2136-tsig-secret-alg=hmac-sha256",
+		"--rfc2136-tsig-axfr",
+		"--rfc2136-min-ttl=30s",
+		"--rfc2136-gss-tsig",
+		"--rfc2136-use-tls",
+		"--rfc2136-skip-tls-verify",
+	)
+	assert.Equal(t, 5353, cfg.RFC2136Port)
+	assert.ElementsMatch(t, []string{"example.org.", "example.com."}, cfg.RFC2136Zone)
+	assert.True(t, cfg.RFC2136CreatePTR)
+	assert.True(t, cfg.RFC2136Insecure)
+	assert.Equal(t, "EXAMPLE.COM", cfg.RFC2136KerberosRealm)
+	assert.Equal(t, "svc-externaldns", cfg.RFC2136KerberosUsername)
+	assert.Equal(t, "secret", cfg.RFC2136KerberosPassword)
+	assert.Equal(t, "keyname.", cfg.RFC2136TSIGKeyName)
+	assert.Equal(t, "base64secret", cfg.RFC2136TSIGSecret)
+	assert.Equal(t, "hmac-sha256", cfg.RFC2136TSIGSecretAlg)
+	assert.True(t, cfg.RFC2136TAXFR)
+	assert.Equal(t, 30*time.Second, cfg.RFC2136MinTTL)
+	assert.True(t, cfg.RFC2136GSSTSIG)
+	assert.True(t, cfg.RFC2136UseTLS)
+	assert.True(t, cfg.RFC2136SkipTLSVerify)
+}
+
+func TestParseFlagsTraefik(t *testing.T) {
+	t.Parallel()
+	cfg := parseCfg(t,
+		"--traefik-enable-legacy",
+		"--traefik-disable-new",
+	)
+	assert.True(t, cfg.TraefikEnableLegacy)
+	assert.True(t, cfg.TraefikDisableNew)
+}
+
+func TestParseFlagsTXTRegistry(t *testing.T) {
+	t.Parallel()
+	cfg := parseCfg(t,
+		"--txt-encrypt-enabled",
+		"--txt-encrypt-aes-key=0123456789abcdef0123456789abcdef",
+		"--txt-suffix=-suffix",
+		"--txt-wildcard-replacement=X",
+	)
+	assert.True(t, cfg.TXTEncryptEnabled)
+	assert.Equal(t, "0123456789abcdef0123456789abcdef", cfg.TXTEncryptAESKey)
+	assert.Equal(t, "-suffix", cfg.TXTSuffix)
+	assert.Equal(t, "X", cfg.TXTWildcardReplacement)
+}
+
+func TestParseFlagsWebhookProvider(t *testing.T) {
+	t.Parallel()
+	cfg := parseCfg(t,
+		"--webhook-provider-url=http://127.0.0.1:9999",
+		"--webhook-provider-read-timeout=7s",
+		"--webhook-provider-write-timeout=8s",
+		"--webhook-server",
+	)
+	assert.Equal(t, "http://127.0.0.1:9999", cfg.WebhookProviderURL)
+	assert.Equal(t, 7*time.Second, cfg.WebhookProviderReadTimeout)
+	assert.Equal(t, 8*time.Second, cfg.WebhookProviderWriteTimeout)
+	assert.True(t, cfg.WebhookServer)
+}
+
+func TestParseFlagsMiscListeners(t *testing.T) {
+	t.Parallel()
+	cfg := parseCfg(t, "--listen-endpoint-events")
+	assert.True(t, cfg.ListenEndpointEvents)
+}
+
+func TestNewCobraCommandValidationMissingProvider(t *testing.T) {
+	cfg := NewConfig()
+	cfg.Provider = ""
+	cfg.Sources = nil
+
+	cmd := newCobraCommand(cfg)
+	cmd.SetArgs([]string{})
+	err := cmd.Execute()
+	require.Error(t, err)
+	assert.Contains(t, err.Error(), "--provider is required")
+}
+
+func TestNewCobraCommandValidationInvalidProvider(t *testing.T) {
+	cfg := NewConfig()
+	cfg.Provider = "not-a-provider"
+	cfg.Sources = []string{"service"}
+
+	cmd := newCobraCommand(cfg)
+	cmd.SetArgs([]string{})
+	err := cmd.Execute()
+	require.Error(t, err)
+	assert.Contains(t, err.Error(), "invalid provider")
+}
+
+func TestNewCobraCommandValidationMissingSource(t *testing.T) {
+	cfg := NewConfig()
+	cfg.Provider = "aws"
+	cfg.Sources = nil
+
+	cmd := newCobraCommand(cfg)
+	cmd.SetArgs([]string{"--provider=aws"})
+	err := cmd.Execute()
+	require.Error(t, err)
+	assert.Contains(t, err.Error(), "--source is required")
+}
+
+func TestNewCobraCommandValidationInvalidSource(t *testing.T) {
+	cfg := NewConfig()
+	cmd := newCobraCommand(cfg)
+	cmd.SetArgs([]string{"--provider=aws", "--source=bogus"})
+	err := cmd.Execute()
+	require.Error(t, err)
+	assert.Contains(t, err.Error(), "invalid source")
+}
+
+func TestNewCobraCommandValidationValid(t *testing.T) {
+	cfg := NewConfig()
+	cmd := newCobraCommand(cfg)
+	cmd.SetArgs([]string{"--provider=aws", "--source=service"})
+	err := cmd.Execute()
+	require.NoError(t, err)
+}
+
+func TestSourceWrapperHelpers(t *testing.T) {
+	cfg := NewConfig()
+	assert.False(t, cfg.IsSourceWrapperInstrumented("nat64"))
+	cfg.AddSourceWrapper("nat64")
+	assert.True(t, cfg.IsSourceWrapperInstrumented("nat64"))
+}
+
+// Accepted binder/backend differences:
+// - Enum validation
+// - Boolean negation form
+// - Env var handling
+
+// Binder parity helpers
+
+// flagKind represents the kind of a bound flag.
+type flagKind int
+
+const (
+	fkString flagKind = iota
+	fkBool
+	fkDuration
+	fkInt
+	fkInt64
+	fkStrings
+	fkEnum
+	fkStringMap
+	fkRegexp
+)
+
+type flagMeta struct {
+	name    string
+	kind    flagKind
+	allowed []string // for enums
+}
+
+// recordingBinder wraps a real binder and records what was bound.
+type recordingBinder struct {
+	inner FlagBinder
+	flags []flagMeta
+}
+
+func (r *recordingBinder) record(name string, kind flagKind, allowed ...string) {
+	r.flags = append(r.flags, flagMeta{name: name, kind: kind, allowed: append([]string(nil), allowed...)})
+}
+
+func (r *recordingBinder) StringVar(name, help, def string, target *string) {
+	r.record(name, fkString)
+	r.inner.StringVar(name, help, def, target)
+}
+func (r *recordingBinder) BoolVar(name, help string, def bool, target *bool) {
+	r.record(name, fkBool)
+	r.inner.BoolVar(name, help, def, target)
+}
+func (r *recordingBinder) DurationVar(name, help string, def time.Duration, target *time.Duration) {
+	r.record(name, fkDuration)
+	r.inner.DurationVar(name, help, def, target)
+}
+func (r *recordingBinder) IntVar(name, help string, def int, target *int) {
+	r.record(name, fkInt)
+	r.inner.IntVar(name, help, def, target)
+}
+func (r *recordingBinder) Int64Var(name, help string, def int64, target *int64) {
+	r.record(name, fkInt64)
+	r.inner.Int64Var(name, help, def, target)
+}
+func (r *recordingBinder) StringsVar(name, help string, def []string, target *[]string) {
+	r.record(name, fkStrings)
+	r.inner.StringsVar(name, help, def, target)
+}
+func (r *recordingBinder) EnumVar(name, help, def string, target *string, allowed ...string) {
+	r.record(name, fkEnum, allowed...)
+	r.inner.EnumVar(name, help, def, target, allowed...)
+}
+func (r *recordingBinder) StringsEnumVar(name, help string, def []string, target *[]string, allowed ...string) {
+	// Not used by bindFlags currently; keep for completeness.
+	r.record(name, fkStrings)
+	r.inner.StringsEnumVar(name, help, def, target, allowed...)
+}
+func (r *recordingBinder) StringMapVar(name, help string, target *map[string]string) {
+	r.record(name, fkStringMap)
+	r.inner.StringMapVar(name, help, target)
+}
+func (r *recordingBinder) RegexpVar(name, help string, def *regexp.Regexp, target **regexp.Regexp) {
+	r.record(name, fkRegexp)
+	r.inner.RegexpVar(name, help, def, target)
+}
+
+// collectAllFlags binds flags to a Cobra binder (which includes provider/source)
+// and records all bound flags for test generation.
+func collectAllFlags(t *testing.T) []flagMeta {
+	t.Helper()
+	cmd := &cobra.Command{Use: "test"}
+	rb := &recordingBinder{inner: NewCobraBinder(cmd)}
+	cfg := &Config{}
+	// mirror helpers used elsewhere to stabilize defaults
+	cfg.AWSSDCreateTag = map[string]string{}
+	cfg.RegexDomainFilter = defaultConfig.RegexDomainFilter
+	bindFlags(rb, cfg)
+
+	// Make names unique and sorted for stable test order
+	byName := map[string]flagMeta{}
+	for _, f := range rb.flags {
+		byName[f.name] = f
+	}
+	names := make([]string, 0, len(byName))
+	for n := range byName {
+		names = append(names, n)
+	}
+	sort.Strings(names)
+	out := make([]flagMeta, 0, len(names))
+	for _, n := range names {
+		out = append(out, byName[n])
+	}
+	return out
+}
+
+func buildArgsCobra(f flagMeta) []string {
+	sw := func(n, v string) string { return fmt.Sprintf("--%s=%s", n, v) }
+	switch f.kind {
+	case fkString:
+		return []string{sw(f.name, "val")}
+	case fkBool:
+		return []string{sw(f.name, "true")}
+	case fkDuration:
+		return []string{sw(f.name, "2s")}
+	case fkInt:
+		return []string{sw(f.name, "42")}
+	case fkInt64:
+		return []string{sw(f.name, "64")}
+	case fkStrings:
+		return []string{sw(f.name, "one"), sw(f.name, "two")}
+	case fkEnum:
+		val := ""
+		for _, a := range f.allowed {
+			if a != "" {
+				val = a
+				break
+			}
+		}
+		if val == "" && len(f.allowed) > 0 {
+			val = f.allowed[0]
+		}
+		if val == "" {
+			val = "x"
+		}
+		return []string{sw(f.name, val)}
+	case fkStringMap:
+		return []string{sw(f.name, "foo=bar")}
+	case fkRegexp:
+		return []string{sw(f.name, "^ex[a-z]+$")}
+	default:
+		return nil
+	}
+}
+
+func buildArgsKingpin(f flagMeta) []string {
+	switch f.kind {
+	case fkString:
+		return []string{"--" + f.name + "=val"}
+	case fkBool:
+		// Kingpin expects --flag=true
+		return []string{"--" + f.name}
+	case fkDuration:
+		return []string{"--" + f.name + "=2s"}
+	case fkInt:
+		return []string{"--" + f.name + "=42"}
+	case fkInt64:
+		return []string{"--" + f.name + "=64"}
+	case fkStrings:
+		return []string{"--" + f.name + "=one", "--" + f.name + "=two"}
+	case fkEnum:
+		val := ""
+		for _, a := range f.allowed {
+			if a != "" {
+				val = a
+				break
+			}
+		}
+		if val == "" && len(f.allowed) > 0 {
+			val = f.allowed[0]
+		}
+		if val == "" {
+			val = "x"
+		}
+		return []string{"--" + f.name + "=" + val}
+	case fkStringMap:
+		return []string{"--" + f.name + "=foo=bar"}
+	case fkRegexp:
+		return []string{"--" + f.name + "=^ex[a-z]+$"}
+	default:
+		return nil
+	}
+}
+
+func normalizeConfig(c *Config) *Config {
+	cp := *c
+	// Regex fields: compare by string; ignore pointer identity
+	cp.RegexDomainFilter = nil
+	cp.RegexDomainExclusion = nil
+	// Treat nil/empty maps equivalently for comparison
+	if cp.AWSSDCreateTag == nil {
+		cp.AWSSDCreateTag = map[string]string{}
+	}
+	// Normalize nil slices to empty slices for parity
+	rv := reflect.ValueOf(&cp).Elem()
+	rt := rv.Type()
+	for i := 0; i < rv.NumField(); i++ {
+		f := rv.Field(i)
+		if f.Kind() == reflect.Slice && f.IsNil() {
+			f.Set(reflect.MakeSlice(rt.Field(i).Type, 0, 0))
+		}
+	}
+	return &cp
+}
+
+func TestBinderParity_AllFlags(t *testing.T) {
+	t.Parallel()
+	flags := collectAllFlags(t)
+
+	// Skip provider/source here: Kingpin does not bind them via bindFlags.
+	base := []string{}
+
+	for _, f := range flags {
+
+		if f.name == "provider" || f.name == "source" {
+			continue
+		}
+		t.Run(f.name, func(t *testing.T) {
+			t.Parallel()
+			argsK := append([]string{}, base...)
+			argsK = append(argsK, buildArgsKingpin(f)...)
+			argsC := append([]string{}, base...)
+			argsC = append(argsC, buildArgsCobra(f)...)
+
+			cfgK := runWithKingpin(t, argsK)
+			cfgC := runWithCobra(t, argsC)
+
+			nK := normalizeConfig(cfgK)
+			nC := normalizeConfig(cfgC)
+
+			if !reflect.DeepEqual(nK, nC) {
+				t.Fatalf("config mismatch for --%s\nKingpin args: %v\nCobra args: %v\nKingpin: %#v\n  Cobra: %#v", f.name, argsK, argsC, nK, nC)
+			}
+		})
+	}
+}
+
+func TestBinderParity_BoolFalse(t *testing.T) {
+	t.Parallel()
+	flags := collectAllFlags(t)
+	for _, f := range flags {
+		if f.kind != fkBool || f.name == "provider" || f.name == "source" {
+			continue
+		}
+
+		t.Run(f.name+"=false", func(t *testing.T) {
+			t.Parallel()
+			// Kingpin: --no-flag
+			argsK := []string{"--no-" + f.name}
+			// Cobra: --flag=false
+			argsC := []string{"--" + f.name + "=false"}
+
+			cfgK := runWithKingpin(t, argsK)
+			cfgC := runWithCobra(t, argsC)
+
+			nK := normalizeConfig(cfgK)
+			nC := normalizeConfig(cfgC)
+			if !reflect.DeepEqual(nK, nC) {
+				t.Fatalf("config mismatch for --%s=false\nKingpin args: %v\nCobra args: %v\nKingpin: %#v\n  Cobra: %#v", f.name, argsK, argsC, nK, nC)
+			}
+		})
+	}
+}
+
+func TestBinderParity_EnumsAllValues(t *testing.T) {
+	t.Parallel()
+	flags := collectAllFlags(t)
+	for _, f := range flags {
+		if f.kind != fkEnum || f.name == "provider" || f.name == "source" {
+			continue
+		}
+		// Deduplicate allowed values
+		seen := map[string]struct{}{}
+		for _, val := range f.allowed {
+			if _, ok := seen[val]; ok {
+				continue
+			}
+			seen[val] = struct{}{}
+			v := val // capture
+			t.Run(f.name+"="+v, func(t *testing.T) {
+				t.Parallel()
+				// Build args for each backend including empty string case
+				var argsK, argsC []string
+				if v == "" {
+					argsK = []string{"--" + f.name + "="}
+					argsC = []string{"--" + f.name + "="}
+				} else {
+					argsK = []string{"--" + f.name + "=" + v}
+					argsC = []string{"--" + f.name + "=" + v}
+				}
+
+				cfgK := runWithKingpin(t, argsK)
+				cfgC := runWithCobra(t, argsC)
+
+				nK := normalizeConfig(cfgK)
+				nC := normalizeConfig(cfgC)
+				if !reflect.DeepEqual(nK, nC) {
+					t.Fatalf("enum config mismatch for --%s=%q\nKingpin: %#v\n  Cobra: %#v", f.name, v, nK, nC)
+				}
+			})
+		}
+	}
+}
+
+// Verify defaults parity (no args) across binders. This asserts that
+// bindFlags applies the same default values irrespective of backend.
+func TestBinderParity_Defaults(t *testing.T) {
+	t.Parallel()
+	cfgK := runWithKingpin(t, []string{})
+	cfgC := runWithCobra(t, []string{})
+
+	nK := normalizeConfig(cfgK)
+	nC := normalizeConfig(cfgC)
+	if !reflect.DeepEqual(nK, nC) {
+		t.Fatalf("defaults config mismatch with no args\nKingpin: %#v\n  Cobra: %#v", nK, nC)
+	}
+}
+
+// Helpers to run bindFlags + parse for each binder.
+func runWithKingpin(t *testing.T, args []string) *Config {
+	t.Helper()
+	cfg := &Config{}
+	cfg.AWSSDCreateTag = map[string]string{}
+	cfg.RegexDomainFilter = defaultConfig.RegexDomainFilter
+	app := kingpin.New("test", "")
+	bindFlags(NewKingpinBinder(app), cfg)
+	_, err := app.Parse(args)
+	require.NoError(t, err)
+	return cfg
+}
+
+func runWithCobra(t *testing.T, args []string) *Config {
+	t.Helper()
+	cfg := &Config{}
+	cfg.AWSSDCreateTag = map[string]string{}
+	cfg.RegexDomainFilter = defaultConfig.RegexDomainFilter
+	cmd := &cobra.Command{Use: "test"}
+	bindFlags(NewCobraBinder(cmd), cfg)
+	cmd.SetArgs(args)
+	err := cmd.Execute()
+	require.NoError(t, err)
+	return cfg
+}
+
+func TestBinderParityScalars(t *testing.T) {
+	cases := []struct {
+		name   string
+		args   []string
+		getter func(*Config) interface{}
+		want   interface{}
+	}{
+		{"fqdn-template", []string{"--fqdn-template=tpl"}, func(c *Config) interface{} { return c.FQDNTemplate }, "tpl"},
+		{"dry-run", []string{"--dry-run"}, func(c *Config) interface{} { return c.DryRun }, true},
+		{"interval", []string{"--interval=2s"}, func(c *Config) interface{} { return c.Interval }, 2 * time.Second},
+		{"google-batch-change-size", []string{"--google-batch-change-size=123"}, func(c *Config) interface{} { return c.GoogleBatchChangeSize }, 123},
+	}
+
+	for _, tc := range cases {
+
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+			cfgK := runWithKingpin(t, tc.args)
+			cfgC := runWithCobra(t, tc.args)
+			assert.Equal(t, tc.want, tc.getter(cfgK))
+			assert.Equal(t, tc.getter(cfgK), tc.getter(cfgC))
+		})
+	}
+}
+
+func TestBinderParityRepeatable(t *testing.T) {
+	args := []string{"--managed-record-types=A", "--managed-record-types=TXT"}
+	cfgK := runWithKingpin(t, args)
+	cfgC := runWithCobra(t, args)
+	assert.ElementsMatch(t, []string{"A", "TXT"}, cfgK.ManagedDNSRecordTypes)
+	assert.ElementsMatch(t, cfgK.ManagedDNSRecordTypes, cfgC.ManagedDNSRecordTypes)
+}
+
+func TestBinderParityMapAndRegexp(t *testing.T) {
+	args := []string{"--regex-domain-filter=^ex.*$", "--aws-sd-create-tag=foo=bar"}
+	cfgK := runWithKingpin(t, args)
+	cfgC := runWithCobra(t, args)
+
+	require.NotNil(t, cfgK.RegexDomainFilter)
+	require.NotNil(t, cfgC.RegexDomainFilter)
+	assert.Equal(t, cfgK.RegexDomainFilter.String(), cfgC.RegexDomainFilter.String())
+
+	require.NotNil(t, cfgK.AWSSDCreateTag)
+	require.NotNil(t, cfgC.AWSSDCreateTag)
+	assert.Equal(t, map[string]string{"foo": "bar"}, cfgK.AWSSDCreateTag)
+	assert.Equal(t, cfgK.AWSSDCreateTag, cfgC.AWSSDCreateTag)
+}
+
+// Kingpin validates enum values at parse time while Cobra does not
+// assert both behaviors so it is obvious and intentional
+func TestBinderEnumValidationDifference(t *testing.T) {
+	// Kingpin should reject unknown enum values
+	appArgs := []string{"--google-zone-visibility=bogus"}
+	app := kingpin.New("test", "")
+	cfgK := &Config{}
+	bindFlags(NewKingpinBinder(app), cfgK)
+	_, err := app.Parse(appArgs)
+	require.Error(t, err)
+
+	// Cobra should accept and set the value
+	cfgC := runWithCobra(t, appArgs)
+	assert.Equal(t, "bogus", cfgC.GoogleZoneVisibility)
+}
diff --git a/provider/cloudflare/cloudflare_test.go b/provider/cloudflare/cloudflare_test.go
index 6afcd33f..6bb7a6bc 100644
--- a/provider/cloudflare/cloudflare_test.go
+++ b/provider/cloudflare/cloudflare_test.go
@@ -1479,60 +1479,139 @@ func TestGroupByNameAndTypeWithCustomHostnames_MX(t *testing.T) {
 }
 
 func TestProviderPropertiesIdempotency(t *testing.T) {
+	t.Parallel()
+
 	testCases := []struct {
-		Name                     string
-		ProviderProxiedByDefault bool
-		RecordsAreProxied        bool
-		ShouldBeUpdated          bool
+		Name                  string
+		SetupProvider         func(*CloudFlareProvider)
+		SetupRecord           func(*dns.RecordResponse)
+		CustomHostnames       []cloudflarev0.CustomHostname
+		RegionKey             string
+		ShouldBeUpdated       bool
+		PropertyKey           string
+		ExpectPropertyPresent bool
+		ExpectPropertyValue   string
 	}{
 		{
-			Name:                     "ProxyDefault: false, ShouldBeProxied: false, ExpectUpdates: false",
-			ProviderProxiedByDefault: false,
-			RecordsAreProxied:        false,
-			ShouldBeUpdated:          false,
+			Name:            "No custom properties, ExpectUpdates: false",
+			SetupProvider:   func(p *CloudFlareProvider) {},
+			SetupRecord:     func(r *dns.RecordResponse) {},
+			ShouldBeUpdated: false,
+		},
+		// Proxied tests
+		{
+			Name:            "ProxiedByDefault: true, ProxiedRecord: true, ExpectUpdates: false",
+			SetupProvider:   func(p *CloudFlareProvider) { p.proxiedByDefault = true },
+			SetupRecord:     func(r *dns.RecordResponse) { r.Proxied = true },
+			ShouldBeUpdated: false,
+		},
+		{
+			Name:                "ProxiedByDefault: true, ProxiedRecord: false, ExpectUpdates: true",
+			SetupProvider:       func(p *CloudFlareProvider) { p.proxiedByDefault = true },
+			SetupRecord:         func(r *dns.RecordResponse) { r.Proxied = false },
+			ShouldBeUpdated:     true,
+			PropertyKey:         annotations.CloudflareProxiedKey,
+			ExpectPropertyValue: "true",
+		},
+		{
+			Name:                "ProxiedByDefault: false, ProxiedRecord: true, ExpectUpdates: true",
+			SetupProvider:       func(p *CloudFlareProvider) { p.proxiedByDefault = false },
+			SetupRecord:         func(r *dns.RecordResponse) { r.Proxied = true },
+			ShouldBeUpdated:     true,
+			PropertyKey:         annotations.CloudflareProxiedKey,
+			ExpectPropertyValue: "false",
+		},
+		// Comment tests
+		{
+			Name:            "DefaultComment: 'foo', RecordComment: 'foo', ExpectUpdates: false",
+			SetupProvider:   func(p *CloudFlareProvider) { p.DNSRecordsConfig.Comment = "foo" },
+			SetupRecord:     func(r *dns.RecordResponse) { r.Comment = "foo" },
+			ShouldBeUpdated: false,
 		},
 		{
-			Name:                     "ProxyDefault: true, ShouldBeProxied: true, ExpectUpdates: false",
-			ProviderProxiedByDefault: true,
-			RecordsAreProxied:        true,
-			ShouldBeUpdated:          false,
+			Name:                  "DefaultComment: '', RecordComment: none, ExpectUpdates: true",
+			SetupProvider:         func(p *CloudFlareProvider) { p.DNSRecordsConfig.Comment = "" },
+			SetupRecord:           func(r *dns.RecordResponse) { r.Comment = "foo" },
+			ShouldBeUpdated:       true,
+			PropertyKey:           annotations.CloudflareRecordCommentKey,
+			ExpectPropertyPresent: false,
 		},
 		{
-			Name:                     "ProxyDefault: true, ShouldBeProxied: false, ExpectUpdates: true",
-			ProviderProxiedByDefault: true,
-			RecordsAreProxied:        false,
-			ShouldBeUpdated:          true,
+			Name:                "DefaultComment: 'foo', RecordComment: 'foo', ExpectUpdates: true",
+			SetupProvider:       func(p *CloudFlareProvider) { p.DNSRecordsConfig.Comment = "foo" },
+			SetupRecord:         func(r *dns.RecordResponse) { r.Comment = "" },
+			ShouldBeUpdated:     true,
+			PropertyKey:         annotations.CloudflareRecordCommentKey,
+			ExpectPropertyValue: "foo",
 		},
+		// Regional Hostname tests
 		{
-			Name:                     "ProxyDefault: false, ShouldBeProxied: true, ExpectUpdates: true",
-			ProviderProxiedByDefault: false,
-			RecordsAreProxied:        true,
-			ShouldBeUpdated:          true,
+			Name: "DefaultRegionKey: 'us', RecordRegionKey: 'us', ExpectUpdates: false",
+			SetupProvider: func(p *CloudFlareProvider) {
+				p.RegionalServicesConfig.Enabled = true
+				p.RegionalServicesConfig.RegionKey = "us"
+			},
+			RegionKey:       "us",
+			ShouldBeUpdated: false,
+		},
+		{
+			Name: "DefaultRegionKey: 'us', RecordRegionKey: 'us', ExpectUpdates: false",
+			SetupProvider: func(p *CloudFlareProvider) {
+				p.RegionalServicesConfig.Enabled = true
+				p.RegionalServicesConfig.RegionKey = "us"
+			},
+			RegionKey:           "eu",
+			ShouldBeUpdated:     true,
+			PropertyKey:         annotations.CloudflareRegionKey,
+			ExpectPropertyValue: "us",
 		},
+		// Custom Hostname tests
+		// TODO: add tests for custom hostnames when properly supported
 	}
 
 	for _, test := range testCases {
 		t.Run(test.Name, func(t *testing.T) {
+			t.Parallel()
+
+			record := dns.RecordResponse{
+				ID:      "1234567890",
+				Name:    "foobar.bar.com",
+				Type:    endpoint.RecordTypeA,
+				TTL:     120,
+				Content: "1.2.3.4",
+			}
+			if test.SetupRecord != nil {
+				test.SetupRecord(&record)
+			}
 			client := NewMockCloudFlareClientWithRecords(map[string][]dns.RecordResponse{
-				"001": {
-					{
-						ID:      "1234567890",
-						Name:    "foobar.bar.com",
-						Type:    endpoint.RecordTypeA,
-						TTL:     120,
-						Content: "1.2.3.4",
-						Proxied: test.RecordsAreProxied,
-					},
-				},
+				"001": {record},
 			})
 
+			if len(test.CustomHostnames) > 0 {
+				customHostnames := make([]cloudflarev0.CustomHostname, 0, len(test.CustomHostnames))
+				for _, ch := range test.CustomHostnames {
+					ch.CustomOriginServer = record.Name
+					customHostnames = append(customHostnames, ch)
+				}
+				client.customHostnames = map[string][]cloudflarev0.CustomHostname{
+					"001": customHostnames,
+				}
+			}
+
+			if test.RegionKey != "" {
+				client.regionalHostnames = map[string][]regionalHostname{
+					"001": {{hostname: record.Name, regionKey: test.RegionKey}},
+				}
+			}
+
 			provider := &CloudFlareProvider{
-				Client:           client,
-				proxiedByDefault: test.ProviderProxiedByDefault,
+				Client: client,
+			}
+			if test.SetupProvider != nil {
+				test.SetupProvider(provider)
 			}
-			ctx := context.Background()
 
-			current, err := provider.Records(ctx)
+			current, err := provider.Records(t.Context())
 			if err != nil {
 				t.Errorf("should not fail, %s", err)
 			}
@@ -1561,19 +1640,31 @@ func TestProviderPropertiesIdempotency(t *testing.T) {
 			}
 
 			plan = *plan.Calculate()
-			assert.NotNil(t, plan.Changes, "should have plan")
-			if plan.Changes == nil {
-				return
-			}
+			require.NotNil(t, plan.Changes, "should have plan")
 			assert.Empty(t, plan.Changes.Create, "should not have creates")
 			assert.Empty(t, plan.Changes.Delete, "should not have deletes")
 
 			if test.ShouldBeUpdated {
-				assert.Len(t, plan.Changes.UpdateNew, 1, "should not have new updates")
-				assert.Len(t, plan.Changes.UpdateOld, 1, "should not have old updates")
+				assert.Len(t, plan.Changes.UpdateOld, 1, "should have old updates")
+				require.Len(t, plan.Changes.UpdateNew, 1, "should have new updates")
+				if test.PropertyKey != "" {
+					value, ok := plan.Changes.UpdateNew[0].GetProviderSpecificProperty(test.PropertyKey)
+					if test.ExpectPropertyPresent || test.ExpectPropertyValue != "" {
+						assert.Truef(t, ok, "should have property %s", test.PropertyKey)
+						assert.Equal(t, test.ExpectPropertyValue, value)
+					} else {
+						assert.Falsef(t, ok, "should not have property %s", test.PropertyKey)
+					}
+				} else {
+					assert.Empty(t, test.ExpectPropertyValue, "test misconfigured, should not expect property value if no property key set")
+					assert.False(t, test.ExpectPropertyPresent, "test misconfigured, should not expect property presence if no property key set")
+				}
 			} else {
 				assert.Empty(t, plan.Changes.UpdateNew, "should not have new updates")
 				assert.Empty(t, plan.Changes.UpdateOld, "should not have old updates")
+				assert.Empty(t, test.PropertyKey, "test misconfigured, should not expect property if no update expected")
+				assert.Empty(t, test.ExpectPropertyValue, "test misconfigured, should not expect property value if no update expected")
+				assert.False(t, test.ExpectPropertyPresent, "test misconfigured, should not expect property presence if no update expected")
 			}
 		})
 	}
diff --git a/source/wrappers/multisource_test.go b/source/wrappers/multisource_test.go
index ea45093f..082ed1c7 100644
--- a/source/wrappers/multisource_test.go
+++ b/source/wrappers/multisource_test.go
@@ -65,13 +65,13 @@ func testMultiSourceEndpoints(t *testing.T) {
 		},
 		{
 			"single non-empty child source returns child's endpoints",
-			[][]*endpoint.Endpoint{{foo}},
-			[]*endpoint.Endpoint{foo},
+			[][]*endpoint.Endpoint{{foo.DeepCopy()}},
+			[]*endpoint.Endpoint{foo.DeepCopy()},
 		},
 		{
 			"multiple non-empty child sources returns merged children's endpoints",
-			[][]*endpoint.Endpoint{{foo}, {bar}},
-			[]*endpoint.Endpoint{foo, bar},
+			[][]*endpoint.Endpoint{{foo.DeepCopy()}, {bar.DeepCopy()}},
+			[]*endpoint.Endpoint{foo.DeepCopy(), bar.DeepCopy()},
 		},
 	} {