diff --git a/api/v1alpha1/externaldns_types.go b/api/v1alpha1/externaldns_types.go index 8e69b736d..a56421d78 100644 --- a/api/v1alpha1/externaldns_types.go +++ b/api/v1alpha1/externaldns_types.go @@ -100,6 +100,14 @@ type ExternalDNSSpec struct { // +kubebuilder:validation:Optional // +optional Zones []string `json:"zones,omitempty"` + + // Interval specifies the interval between two consecutive synchronizations + // performed by ExternalDNS. When omitted, ExternalDNS uses its default + // interval of 1 minute. + // + // +kubebuilder:validation:Optional + // +optional + Interval *metav1.Duration `json:"interval,omitempty"` } // ExternalDNSDomain describes how sets of included @@ -363,6 +371,15 @@ type ExternalDNSInfobloxProviderOptions struct { // +kubebuilder:validation:Required // +required WAPIVersion string `json:"wapiVersion"` + + // MaxResults sets the _max_results query parameter on Infoblox WAPI GET + // requests. This should be set when integrating with Infoblox grids that + // contain a large number of DNS records. + // + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Minimum=1 + // +optional + MaxResults *int `json:"maxResults,omitempty"` } // SecretReference contains the information to let you locate the desired secret. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index ae80521be..c8989571d 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -187,6 +187,11 @@ func (in *ExternalDNSGCPProviderOptions) DeepCopy() *ExternalDNSGCPProviderOptio func (in *ExternalDNSInfobloxProviderOptions) DeepCopyInto(out *ExternalDNSInfobloxProviderOptions) { *out = *in out.Credentials = in.Credentials + if in.MaxResults != nil { + in, out := &in.MaxResults, &out.MaxResults + *out = new(int) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalDNSInfobloxProviderOptions. @@ -272,7 +277,7 @@ func (in *ExternalDNSProvider) DeepCopyInto(out *ExternalDNSProvider) { if in.Infoblox != nil { in, out := &in.Infoblox, &out.Infoblox *out = new(ExternalDNSInfobloxProviderOptions) - **out = **in + (*in).DeepCopyInto(*out) } } @@ -374,6 +379,11 @@ func (in *ExternalDNSSpec) DeepCopyInto(out *ExternalDNSSpec) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.Interval != nil { + in, out := &in.Interval, &out.Interval + *out = new(v1.Duration) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalDNSSpec. diff --git a/api/v1beta1/externaldns_types.go b/api/v1beta1/externaldns_types.go index 4af220a47..c73b5e89a 100644 --- a/api/v1beta1/externaldns_types.go +++ b/api/v1beta1/externaldns_types.go @@ -101,6 +101,14 @@ type ExternalDNSSpec struct { // +kubebuilder:validation:Optional // +optional Zones []string `json:"zones,omitempty"` + + // Interval specifies the interval between two consecutive synchronizations + // performed by ExternalDNS. When omitted, ExternalDNS uses its default + // interval of 1 minute. + // + // +kubebuilder:validation:Optional + // +optional + Interval *metav1.Duration `json:"interval,omitempty"` } // ExternalDNSDomain describes how sets of included @@ -361,6 +369,15 @@ type ExternalDNSInfobloxProviderOptions struct { // +kubebuilder:validation:Required // +required WAPIVersion string `json:"wapiVersion"` + + // MaxResults sets the _max_results query parameter on Infoblox WAPI GET + // requests. This should be set when integrating with Infoblox grids that + // contain a large number of DNS records. + // + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Minimum=1 + // +optional + MaxResults *int `json:"maxResults,omitempty"` } // SecretReference contains the information to let you locate the desired secret. diff --git a/api/v1beta1/externaldns_webhook.go b/api/v1beta1/externaldns_webhook.go index 87d14763b..a1f65a6ec 100644 --- a/api/v1beta1/externaldns_webhook.go +++ b/api/v1beta1/externaldns_webhook.go @@ -73,6 +73,8 @@ func (r *ExternalDNS) validate(old runtime.Object) error { r.validateHostnameAnnotationPolicy(), r.validateProviderCredentials(), r.validateAWSRoleARN(), + r.validateInterval(), + r.validateInfobloxMaxResults(), }) } @@ -168,3 +170,17 @@ func (r *ExternalDNS) validateAWSRoleARN() error { return nil } + +func (r *ExternalDNS) validateInterval() error { + if r.Spec.Interval != nil && r.Spec.Interval.Duration <= 0 { + return errors.New(`"interval" must be greater than zero when specified`) + } + return nil +} + +func (r *ExternalDNS) validateInfobloxMaxResults() error { + if r.Spec.Provider.Infoblox != nil && r.Spec.Provider.Infoblox.MaxResults != nil && *r.Spec.Provider.Infoblox.MaxResults <= 0 { + return errors.New(`"maxResults" must be greater than zero when specified for Infoblox provider`) + } + return nil +} diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index f3a82da2e..8e82ecbad 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -207,6 +207,11 @@ func (in *ExternalDNSGCPProviderOptions) DeepCopy() *ExternalDNSGCPProviderOptio func (in *ExternalDNSInfobloxProviderOptions) DeepCopyInto(out *ExternalDNSInfobloxProviderOptions) { *out = *in out.Credentials = in.Credentials + if in.MaxResults != nil { + in, out := &in.MaxResults, &out.MaxResults + *out = new(int) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalDNSInfobloxProviderOptions. @@ -292,7 +297,7 @@ func (in *ExternalDNSProvider) DeepCopyInto(out *ExternalDNSProvider) { if in.Infoblox != nil { in, out := &in.Infoblox, &out.Infoblox *out = new(ExternalDNSInfobloxProviderOptions) - **out = **in + (*in).DeepCopyInto(*out) } } @@ -394,6 +399,11 @@ func (in *ExternalDNSSpec) DeepCopyInto(out *ExternalDNSSpec) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.Interval != nil { + in, out := &in.Interval, &out.Interval + *out = new(v1.Duration) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalDNSSpec. diff --git a/config/crd/bases/externaldns.olm.openshift.io_externaldnses.yaml b/config/crd/bases/externaldns.olm.openshift.io_externaldnses.yaml index f03bb9f6f..0c1aed215 100644 --- a/config/crd/bases/externaldns.olm.openshift.io_externaldnses.yaml +++ b/config/crd/bases/externaldns.olm.openshift.io_externaldnses.yaml @@ -126,6 +126,12 @@ spec: - matchType type: object type: array + interval: + description: |- + Interval specifies the interval between two consecutive synchronizations + performed by ExternalDNS. When omitted, ExternalDNS uses its default + interval of 1 minute. + type: string provider: description: |- Provider refers to the DNS provider that ExternalDNS @@ -282,6 +288,13 @@ spec: gridHost: description: GridHost is the IP of the Infoblox Grid host. type: string + maxResults: + description: |- + MaxResults sets the _max_results query parameter on Infoblox WAPI GET + requests. This should be set when integrating with Infoblox grids that + contain a large number of DNS records. + minimum: 1 + type: integer wapiPort: description: WAPIPort is the port for the Infoblox WAPI. type: integer @@ -692,6 +705,12 @@ spec: - matchType type: object type: array + interval: + description: |- + Interval specifies the interval between two consecutive synchronizations + performed by ExternalDNS. When omitted, ExternalDNS uses its default + interval of 1 minute. + type: string provider: description: |- Provider refers to the DNS provider that ExternalDNS @@ -855,6 +874,13 @@ spec: gridHost: description: GridHost is the IP of the Infoblox Grid host. type: string + maxResults: + description: |- + MaxResults sets the _max_results query parameter on Infoblox WAPI GET + requests. This should be set when integrating with Infoblox grids that + contain a large number of DNS records. + minimum: 1 + type: integer wapiPort: description: WAPIPort is the port for the Infoblox WAPI. type: integer diff --git a/config/samples/infoblox/operator_v1beta1_infoblox_openshift.yaml b/config/samples/infoblox/operator_v1beta1_infoblox_openshift.yaml index 93edacf23..9eb209f62 100644 --- a/config/samples/infoblox/operator_v1beta1_infoblox_openshift.yaml +++ b/config/samples/infoblox/operator_v1beta1_infoblox_openshift.yaml @@ -15,6 +15,8 @@ spec: gridHost: "100.100.100.100" wapiPort: 443 wapiVersion: "2.12.2" + maxResults: 2000 + interval: 5m source: # Source Type is route resource of OpenShift type: OpenShiftRoute diff --git a/docs/usage.md b/docs/usage.md index 490ba9de0..2bab3f514 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -210,6 +210,8 @@ the following information is required: gridHost: # the grid master host from the previous step. eg: 172.26.1.200 wapiPort: # the WAPI port, eg: 80, 443, 8080 wapiVersion: # the WAPI version, eg: 2.11, 2.3.1 + maxResults: 2000 # optional, sets --infoblox-max-results for large Infoblox grids + interval: 5m # optional, sets --interval for sync frequency zones: # Replace with the desired hosted zones - "ZG5zLm5ldHdvcmtfdmlldyQw" source: diff --git a/pkg/operator/controller/externaldns/pod.go b/pkg/operator/controller/externaldns/pod.go index be6ab44eb..6c86c389e 100644 --- a/pkg/operator/controller/externaldns/pod.go +++ b/pkg/operator/controller/externaldns/pod.go @@ -215,6 +215,10 @@ func (b *externalDNSContainerBuilder) fillProviderAgnosticFields(seq int, zone s args = append(args, fmt.Sprintf("--openshift-router-name=%s", b.externalDNS.Spec.Source.OpenShiftRoute.RouterName)) } + if b.externalDNS.Spec.Interval != nil && b.externalDNS.Spec.Interval.Duration > 0 { + args = append(args, fmt.Sprintf("--interval=%s", b.externalDNS.Spec.Interval.Duration.String())) + } + filterArgs, err := b.domainFilters() if err != nil { return err @@ -479,6 +483,10 @@ func (b *externalDNSContainerBuilder) fillInfobloxFields(container *corev1.Conta args = append(args, fmt.Sprintf("--infoblox-wapi-version=%s", b.externalDNS.Spec.Provider.Infoblox.WAPIVersion)) } + if b.externalDNS.Spec.Provider.Infoblox.MaxResults != nil && *b.externalDNS.Spec.Provider.Infoblox.MaxResults > 0 { + args = append(args, fmt.Sprintf("--infoblox-max-results=%d", *b.externalDNS.Spec.Provider.Infoblox.MaxResults)) + } + args = addTXTPrefixFlag(args) env := []corev1.EnvVar{ diff --git a/pkg/operator/controller/externaldns/pod_interval_test.go b/pkg/operator/controller/externaldns/pod_interval_test.go new file mode 100644 index 000000000..2d1e62857 --- /dev/null +++ b/pkg/operator/controller/externaldns/pod_interval_test.go @@ -0,0 +1,136 @@ +package externaldnscontroller + +import ( + "strings" + "testing" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + + "github.com/openshift/external-dns-operator/api/v1beta1" +) + +func TestIntervalArg(t *testing.T) { + for _, tc := range []struct { + name string + interval *metav1.Duration + expectInterval bool + expectedInterval string + }{ + { + name: "interval omitted", + interval: nil, + expectInterval: false, + }, + { + name: "interval set", + interval: &metav1.Duration{Duration: 5 * time.Minute}, + expectInterval: true, + expectedInterval: "--interval=5m0s", + }, + } { + t.Run(tc.name, func(t *testing.T) { + b := &externalDNSContainerBuilder{ + externalDNS: &v1beta1.ExternalDNS{ + Spec: v1beta1.ExternalDNSSpec{ + Interval: tc.interval, + Source: v1beta1.ExternalDNSSource{ + ExternalDNSSourceUnion: v1beta1.ExternalDNSSourceUnion{ + Type: v1beta1.SourceTypeRoute, + OpenShiftRoute: &v1beta1.ExternalDNSOpenShiftRouteOptions{ + RouterName: "default", + }, + }, + HostnameAnnotationPolicy: v1beta1.HostnameAnnotationPolicyIgnore, + }, + }, + }, + provider: externalDNSProviderTypeInfoblox, + source: "openshift-route", + } + + container := b.defaultContainer("external-dns") + if err := b.fillProviderAgnosticFields(0, "", container); err != nil { + t.Fatalf("unexpected error: %v", err) + } + + var intervalArg string + for _, arg := range container.Args { + if strings.HasPrefix(arg, "--interval=") { + intervalArg = arg + } + } + + if tc.expectInterval { + if intervalArg != tc.expectedInterval { + t.Fatalf("expected %q, got %q from %v", tc.expectedInterval, intervalArg, container.Args) + } + return + } + + if intervalArg != "" { + t.Fatalf("unexpected interval arg %q in %v", intervalArg, container.Args) + } + }) + } +} + +func TestInfobloxMaxResultsArg(t *testing.T) { + for _, tc := range []struct { + name string + maxResults *int + expectedArgs []string + }{ + { + name: "max results omitted", + maxResults: nil, + expectedArgs: nil, + }, + { + name: "max results set", + maxResults: ptr.To[int](2000), + expectedArgs: []string{"--infoblox-max-results=2000"}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + b := &externalDNSContainerBuilder{ + externalDNS: &v1beta1.ExternalDNS{ + Spec: v1beta1.ExternalDNSSpec{ + Provider: v1beta1.ExternalDNSProvider{ + Type: v1beta1.ProviderTypeInfoblox, + Infoblox: &v1beta1.ExternalDNSInfobloxProviderOptions{ + GridHost: "gridhost.example.com", + WAPIPort: 443, + WAPIVersion: "2.12.2", + MaxResults: tc.maxResults, + }, + }, + }, + }, + secretName: "infoblox-credentials", + } + + container := b.defaultContainer("external-dns") + b.fillInfobloxFields(container) + + var got []string + for _, arg := range container.Args { + if strings.HasPrefix(arg, "--infoblox-max-results=") { + got = append(got, arg) + } + } + + if tc.expectedArgs == nil { + if len(got) != 0 { + t.Fatalf("unexpected max results args %v", got) + } + return + } + + if len(got) != 1 || got[0] != tc.expectedArgs[0] { + t.Fatalf("expected max results args %v, got %v from %v", tc.expectedArgs, got, container.Args) + } + }) + } +}