diff --git a/PROJECT b/PROJECT index d9a3cea..31d331e 100644 --- a/PROJECT +++ b/PROJECT @@ -8,21 +8,6 @@ layout: projectName: powerdns-operator repo: github.com/powerdns-operator/powerdns-operator resources: -- api: - crdVersion: v1 - domain: cav.enablers.ob - group: dns - kind: Zone - path: github.com/powerdns-operator/powerdns-operator/api/v1alpha1 - version: v1alpha1 -- api: - crdVersion: v1 - namespaced: true - domain: cav.enablers.ob - group: dns - kind: RRset - path: github.com/powerdns-operator/powerdns-operator/api/v1alpha1 - version: v1alpha1 - api: crdVersion: v1 namespaced: true @@ -30,8 +15,8 @@ resources: domain: cav.enablers.ob group: dns kind: Zone - path: github.com/powerdns-operator/powerdns-operator/api/v1alpha2 - version: v1alpha2 + path: github.com/powerdns-operator/powerdns-operator/api/v1alpha3 + version: v1alpha3 - api: crdVersion: v1 namespaced: true @@ -39,22 +24,30 @@ resources: domain: cav.enablers.ob group: dns kind: RRset - path: github.com/powerdns-operator/powerdns-operator/api/v1alpha2 - version: v1alpha2 + path: github.com/powerdns-operator/powerdns-operator/api/v1alpha3 + version: v1alpha3 - api: crdVersion: v1 controller: true domain: cav.enablers.ob group: dns kind: ClusterZone - path: github.com/powerdns-operator/powerdns-operator/api/v1alpha2 - version: v1alpha2 + path: github.com/powerdns-operator/powerdns-operator/api/v1alpha3 + version: v1alpha3 - api: crdVersion: v1 controller: true domain: cav.enablers.ob group: dns kind: ClusterRRset - path: github.com/powerdns-operator/powerdns-operator/api/v1alpha2 - version: v1alpha2 + path: github.com/powerdns-operator/powerdns-operator/api/v1alpha3 + version: v1alpha3 +- api: + crdVersion: v1 + controller: true + domain: cav.enablers.ob + group: dns + kind: PDNSProvider + path: github.com/powerdns-operator/powerdns-operator/api/v1alpha3 + version: v1alpha3 version: "3" diff --git a/README.md b/README.md index 0b04581..34718e6 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ https://github.com/user-attachments/assets/cc43b03e-ed0d-4112-941d-0b53cc2ad3be |-----------|-------------------| | **PowerDNS Authoritative** | 4.7, 4.8, 4.9 | | **Kubernetes** | 1.31, 1.32, 1.33 | -| **Go** (for development) | 1.24+ | +| **Go** (for development) | 1.25+ | ## 🛠️ Installation diff --git a/api/v1alpha1/rrset_types.go b/api/v1alpha1/rrset_types.go index cd97b2d..787cb8d 100644 --- a/api/v1alpha1/rrset_types.go +++ b/api/v1alpha1/rrset_types.go @@ -48,6 +48,7 @@ type RRsetStatus struct { } // +kubebuilder:object:root=true +// +kubebuilder:storageversion:deprecated // +kubebuilder:subresource:status // +kubebuilder:unservedversion diff --git a/api/v1alpha1/zone_types.go b/api/v1alpha1/zone_types.go index 49f45c3..124a01a 100644 --- a/api/v1alpha1/zone_types.go +++ b/api/v1alpha1/zone_types.go @@ -69,6 +69,7 @@ type ZoneStatus struct { } // +kubebuilder:object:root=true +// +kubebuilder:storageversion:deprecated // +kubebuilder:subresource:status // +kubebuilder:unservedversion diff --git a/api/v1alpha2/clusterrrset_types.go b/api/v1alpha2/clusterrrset_types.go index c9be8ea..e0f10ef 100644 --- a/api/v1alpha2/clusterrrset_types.go +++ b/api/v1alpha2/clusterrrset_types.go @@ -15,9 +15,10 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -//+kubebuilder:object:root=true -//+kubebuilder:subresource:status -//+kubebuilder:resource:scope=Cluster +// +kubebuilder:object:root=true +// +kubebuilder:storageversion:deprecated +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Cluster // +kubebuilder:printcolumn:name="Zone",type="string",JSONPath=".spec.zoneRef.name" // +kubebuilder:printcolumn:name="Name",type="string",JSONPath=".status.dnsEntryName" @@ -34,7 +35,7 @@ type ClusterRRset struct { Status RRsetStatus `json:"status,omitempty"` } -//+kubebuilder:object:root=true +// +kubebuilder:object:root=true // ClusterRRsetList contains a list of ClusterRRset type ClusterRRsetList struct { diff --git a/api/v1alpha2/clusterzone_types.go b/api/v1alpha2/clusterzone_types.go index 7af7f99..947aaaa 100644 --- a/api/v1alpha2/clusterzone_types.go +++ b/api/v1alpha2/clusterzone_types.go @@ -15,8 +15,9 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -//+kubebuilder:object:root=true -//+kubebuilder:subresource:status +// +kubebuilder:object:root=true +// +kubebuilder:storageversion:deprecated +// +kubebuilder:subresource:status // +kubebuilder:resource:scope=Cluster // +kubebuilder:printcolumn:name="Serial",type="integer",JSONPath=".status.serial" @@ -31,7 +32,7 @@ type ClusterZone struct { Status ZoneStatus `json:"status,omitempty"` } -//+kubebuilder:object:root=true +// +kubebuilder:object:root=true // ClusterZoneList contains a list of ClusterZone type ClusterZoneList struct { diff --git a/api/v1alpha2/rrset_types.go b/api/v1alpha2/rrset_types.go index a22f674..ea2edb6 100644 --- a/api/v1alpha2/rrset_types.go +++ b/api/v1alpha2/rrset_types.go @@ -50,11 +50,11 @@ type RRsetStatus struct { ObservedGeneration *int64 `json:"observedGeneration,omitempty"` } -//+kubebuilder:object:root=true -//+kubebuilder:storageversion -//+kubebuilder:conversion:hub -//+kubebuilder:subresource:status -//+kubebuilder:resource:scope=Namespaced +// +kubebuilder:object:root=true +// +kubebuilder:storageversion:deprecated +// +kubebuilder:conversion:hub +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Namespaced // +kubebuilder:printcolumn:name="Zone",type="string",JSONPath=".spec.zoneRef.name" // +kubebuilder:printcolumn:name="Name",type="string",JSONPath=".status.dnsEntryName" @@ -71,7 +71,7 @@ type RRset struct { Status RRsetStatus `json:"status,omitempty"` } -//+kubebuilder:object:root=true +// +kubebuilder:object:root=true // RRsetList contains a list of RRset type RRsetList struct { diff --git a/api/v1alpha2/zone_types.go b/api/v1alpha2/zone_types.go index c1f7ae9..4505044 100644 --- a/api/v1alpha2/zone_types.go +++ b/api/v1alpha2/zone_types.go @@ -68,10 +68,10 @@ type ZoneStatus struct { ObservedGeneration *int64 `json:"observedGeneration,omitempty"` } -//+kubebuilder:object:root=true -//+kubebuilder:storageversion -//+kubebuilder:subresource:status -//+kubebuilder:resource:scope=Namespaced +// +kubebuilder:object:root=true +// +kubebuilder:storageversion:deprecated +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Namespaced // +kubebuilder:printcolumn:name="Serial",type="integer",JSONPath=".status.serial" // +kubebuilder:printcolumn:name="ID",type="string",JSONPath=".status.id" @@ -85,7 +85,7 @@ type Zone struct { Status ZoneStatus `json:"status,omitempty"` } -//+kubebuilder:object:root=true +// +kubebuilder:object:root=true // ZoneList contains a list of Zone type ZoneList struct { diff --git a/api/v1alpha3/clusterrrset_types.go b/api/v1alpha3/clusterrrset_types.go new file mode 100644 index 0000000..c808b53 --- /dev/null +++ b/api/v1alpha3/clusterrrset_types.go @@ -0,0 +1,57 @@ +/* + * Software Name : PowerDNS-Operator + * + * SPDX-FileCopyrightText: Copyright (c) PowerDNS-Operator contributors + * SPDX-FileCopyrightText: Copyright (c) 2025 Orange Business Services SA + * SPDX-License-Identifier: Apache-2.0 + * + * This software is distributed under the Apache 2.0 License, + * see the "LICENSE" file for more details + */ + +package v1alpha3 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +kubebuilder:object:root=true +// +kubebuilder:storageversion +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Cluster + +// +kubebuilder:printcolumn:name="Zone",type="string",JSONPath=".spec.zoneRef.name" +// +kubebuilder:printcolumn:name="Name",type="string",JSONPath=".status.dnsEntryName" +// +kubebuilder:printcolumn:name="Type",type="string",JSONPath=".spec.type" +// +kubebuilder:printcolumn:name="TTL",type="integer",JSONPath=".spec.ttl" +// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.syncStatus" +// +kubebuilder:printcolumn:name="Records",type="string",JSONPath=".spec.records" +// ClusterRRset is the Schema for the clusterrrsets API +type ClusterRRset struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec RRsetSpec `json:"spec,omitempty"` + Status RRsetStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// ClusterRRsetList contains a list of ClusterRRset +type ClusterRRsetList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ClusterRRset `json:"items"` +} + +func init() { + SchemeBuilder.Register(&ClusterRRset{}, &ClusterRRsetList{}) +} + +// IsInExpectedStatus returns true if Status.SyncStatus and Status.ObservedGeneration are, at least, at expected value +func (r *ClusterRRset) IsInExpectedStatus(expectedMinimumObservedGeneration int64, expectedSyncStatus string) bool { + return r.Status.ObservedGeneration != nil && + *r.Status.ObservedGeneration >= expectedMinimumObservedGeneration && + r.Status.SyncStatus != nil && + *r.Status.SyncStatus == expectedSyncStatus +} diff --git a/api/v1alpha3/clusterzone_types.go b/api/v1alpha3/clusterzone_types.go new file mode 100644 index 0000000..ed3c2bf --- /dev/null +++ b/api/v1alpha3/clusterzone_types.go @@ -0,0 +1,55 @@ +/* + * Software Name : PowerDNS-Operator + * + * SPDX-FileCopyrightText: Copyright (c) PowerDNS-Operator contributors + * SPDX-FileCopyrightText: Copyright (c) 2025 Orange Business Services SA + * SPDX-License-Identifier: Apache-2.0 + * + * This software is distributed under the Apache 2.0 License, + * see the "LICENSE" file for more details + */ + +package v1alpha3 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +kubebuilder:object:root=true +// +kubebuilder:storageversion +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Cluster + +// +kubebuilder:printcolumn:name="PDNSProvider",type="string",JSONPath=".spec.providerRef" +// +kubebuilder:printcolumn:name="Serial",type="integer",JSONPath=".status.serial" +// +kubebuilder:printcolumn:name="ID",type="string",JSONPath=".status.id" +// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.syncStatus" +// ClusterZone is the Schema for the clusterzones API +type ClusterZone struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ZoneSpec `json:"spec,omitempty"` + Status ZoneStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// ClusterZoneList contains a list of ClusterZone +type ClusterZoneList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ClusterZone `json:"items"` +} + +func init() { + SchemeBuilder.Register(&ClusterZone{}, &ClusterZoneList{}) +} + +// IsInExpectedStatus returns true if Status.SyncStatus and Status.ObservedGeneration are, at least, at expected value +func (z *ClusterZone) IsInExpectedStatus(expectedMinimumObservedGeneration int64, expectedSyncStatus string) bool { + return z.Status.ObservedGeneration != nil && + *z.Status.ObservedGeneration >= expectedMinimumObservedGeneration && + z.Status.SyncStatus != nil && + *z.Status.SyncStatus == expectedSyncStatus +} diff --git a/api/v1alpha3/generic_rrset.go b/api/v1alpha3/generic_rrset.go new file mode 100644 index 0000000..bd2aab9 --- /dev/null +++ b/api/v1alpha3/generic_rrset.go @@ -0,0 +1,93 @@ +/* + * Software Name : PowerDNS-Operator + * + * SPDX-FileCopyrightText: Copyright (c) PowerDNS-Operator contributors + * SPDX-FileCopyrightText: Copyright (c) 2025 Orange Business Services SA + * SPDX-License-Identifier: Apache-2.0 + * + * This software is distributed under the Apache 2.0 License, + * see the "LICENSE" file for more details + */ + +//nolint:dupl +package v1alpha3 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// +kubebuilder:object:root=false +// +kubebuilder:object:generate:false +// +k8s:deepcopy-gen:interfaces=nil +// +k8s:deepcopy-gen=nil + +// GenericRRset is a common interface for interacting with ClusterRRset or a namespaced RRset. +type GenericRRset interface { + runtime.Object + metav1.Object + + GetObjectMeta() *metav1.ObjectMeta + GetTypeMeta() *metav1.TypeMeta + + GetSpec() *RRsetSpec + GetStatus() RRsetStatus + SetStatus(status RRsetStatus) + Copy() GenericRRset +} + +// +kubebuilder:object:root:false +// +kubebuilder:object:generate:false +var _ GenericRRset = &RRset{} + +func (c *RRset) GetObjectMeta() *metav1.ObjectMeta { + return &c.ObjectMeta +} + +func (c *RRset) GetTypeMeta() *metav1.TypeMeta { + return &c.TypeMeta +} + +func (c *RRset) GetSpec() *RRsetSpec { + return &c.Spec +} + +func (c *RRset) GetStatus() RRsetStatus { + return c.Status +} + +func (c *RRset) SetStatus(status RRsetStatus) { + c.Status = status +} + +func (c *RRset) Copy() GenericRRset { + return c.DeepCopy() +} + +// +kubebuilder:object:root:false +// +kubebuilder:object:generate:false +var _ GenericRRset = &ClusterRRset{} + +func (c *ClusterRRset) GetObjectMeta() *metav1.ObjectMeta { + return &c.ObjectMeta +} + +func (c *ClusterRRset) GetTypeMeta() *metav1.TypeMeta { + return &c.TypeMeta +} + +func (c *ClusterRRset) GetSpec() *RRsetSpec { + return &c.Spec +} + +func (c *ClusterRRset) GetStatus() RRsetStatus { + return c.Status +} + +func (c *ClusterRRset) SetStatus(status RRsetStatus) { + c.Status = status +} + +func (c *ClusterRRset) Copy() GenericRRset { + return c.DeepCopy() +} diff --git a/api/v1alpha3/generic_zone.go b/api/v1alpha3/generic_zone.go new file mode 100644 index 0000000..c66f161 --- /dev/null +++ b/api/v1alpha3/generic_zone.go @@ -0,0 +1,103 @@ +/* + * Software Name : PowerDNS-Operator + * + * SPDX-FileCopyrightText: Copyright (c) PowerDNS-Operator contributors + * SPDX-FileCopyrightText: Copyright (c) 2025 Orange Business Services SA + * SPDX-License-Identifier: Apache-2.0 + * + * This software is distributed under the Apache 2.0 License, + * see the "LICENSE" file for more details + */ + +//nolint:dupl +package v1alpha3 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// +kubebuilder:object:root=false +// +kubebuilder:object:generate:false +// +k8s:deepcopy-gen:interfaces=nil +// +k8s:deepcopy-gen=nil + +// GenericZone is a common interface for interacting with ClusterZone +// or a namespaced Zone. +type GenericZone interface { + runtime.Object + metav1.Object + + GetObjectMeta() *metav1.ObjectMeta + GetTypeMeta() *metav1.TypeMeta + + GetSpec() *ZoneSpec + GetStatus() ZoneStatus + SetStatus(status ZoneStatus) + GetProviderRef() string + Copy() GenericZone +} + +// +kubebuilder:object:root:false +// +kubebuilder:object:generate:false +var _ GenericZone = &Zone{} + +func (c *Zone) GetObjectMeta() *metav1.ObjectMeta { + return &c.ObjectMeta +} + +func (c *Zone) GetTypeMeta() *metav1.TypeMeta { + return &c.TypeMeta +} + +func (c *Zone) GetSpec() *ZoneSpec { + return &c.Spec +} + +func (c *Zone) GetStatus() ZoneStatus { + return c.Status +} + +func (c *Zone) SetStatus(status ZoneStatus) { + c.Status = status +} + +func (c *Zone) GetProviderRef() string { + return c.Spec.ProviderRef +} + +func (c *Zone) Copy() GenericZone { + return c.DeepCopy() +} + +// +kubebuilder:object:root:false +// +kubebuilder:object:generate:false +var _ GenericZone = &ClusterZone{} + +func (c *ClusterZone) GetObjectMeta() *metav1.ObjectMeta { + return &c.ObjectMeta +} + +func (c *ClusterZone) GetTypeMeta() *metav1.TypeMeta { + return &c.TypeMeta +} + +func (c *ClusterZone) GetSpec() *ZoneSpec { + return &c.Spec +} + +func (c *ClusterZone) GetStatus() ZoneStatus { + return c.Status +} + +func (c *ClusterZone) SetStatus(status ZoneStatus) { + c.Status = status +} + +func (c *ClusterZone) GetProviderRef() string { + return c.Spec.ProviderRef +} + +func (c *ClusterZone) Copy() GenericZone { + return c.DeepCopy() +} diff --git a/api/v1alpha3/groupversion_info.go b/api/v1alpha3/groupversion_info.go new file mode 100644 index 0000000..684246c --- /dev/null +++ b/api/v1alpha3/groupversion_info.go @@ -0,0 +1,31 @@ +/* + * Software Name : PowerDNS-Operator + * + * SPDX-FileCopyrightText: Copyright (c) PowerDNS-Operator contributors + * SPDX-FileCopyrightText: Copyright (c) 2025 Orange Business Services SA + * SPDX-License-Identifier: Apache-2.0 + * + * This software is distributed under the Apache 2.0 License, + * see the "LICENSE" file for more details + */ + +// Package v1alpha3 contains API Schema definitions for the dns v1alpha3 API group. +// +kubebuilder:object:generate=true +// +groupName=dns.cav.enablers.ob +package v1alpha3 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects. + GroupVersion = schema.GroupVersion{Group: "dns.cav.enablers.ob", Version: "v1alpha3"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme. + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/api/v1alpha3/pdnsprovider_types.go b/api/v1alpha3/pdnsprovider_types.go new file mode 100644 index 0000000..23e663b --- /dev/null +++ b/api/v1alpha3/pdnsprovider_types.go @@ -0,0 +1,263 @@ +/* + * Software Name : PowerDNS-Operator + * + * SPDX-FileCopyrightText: Copyright (c) PowerDNS-Operator contributors + * SPDX-License-Identifier: Apache-2.0 + * + * This software is distributed under the Apache 2.0 License, + * see the "LICENSE" file for more details + */ + +package v1alpha3 + +import ( + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// PDNSProviderTLSConfig defines TLS configuration for PowerDNS API connection +type PDNSProviderTLSConfig struct { + // Insecure enables insecure connections to PowerDNS API (skip TLS verification) + // +kubebuilder:default:=false + // +optional + Insecure *bool `json:"insecure,omitempty"` + + // CABundleRef is a reference to a ConfigMap or Secret containing a CA bundle + // +optional + CABundleRef *PDNSProviderCABundleRef `json:"caBundleRef,omitempty"` +} + +// PDNSProviderCABundleRef defines a reference to a CA bundle in a ConfigMap or Secret +type PDNSProviderCABundleRef struct { + // Name is the name of the ConfigMap or Secret + // +kubebuilder:validation:Required + Name string `json:"name"` + + // Namespace is the namespace of the ConfigMap or Secret + // If not specified, defaults to the operator namespace + // +optional + Namespace *string `json:"namespace,omitempty"` + + // Kind is the kind of resource (ConfigMap or Secret) + // +kubebuilder:validation:Enum=ConfigMap;Secret + // +kubebuilder:default:="ConfigMap" + // +optional + Kind *string `json:"kind,omitempty"` + + // Key is the key in the ConfigMap or Secret containing the CA bundle + // +kubebuilder:default:="ca.crt" + // +optional + Key *string `json:"key,omitempty"` +} + +// PDNSProviderCredentials defines credentials configuration for PowerDNS API +type PDNSProviderCredentials struct { + // SecretRef is a reference to a Kubernetes Secret containing the PowerDNS API key + // +kubebuilder:validation:Required + SecretRef PDNSProviderSecretRef `json:"secretRef"` +} + +// PDNSProviderSecretRef defines a reference to a Secret containing API credentials +type PDNSProviderSecretRef struct { + // Name is the name of the Secret + // +kubebuilder:validation:Required + Name string `json:"name"` + + // Namespace is the namespace of the Secret + // If not specified, defaults to the PDNSProvider resource namespace + // +optional + Namespace *string `json:"namespace,omitempty"` + + // Key is the key in the Secret containing the API key + // +kubebuilder:default:="apiKey" + // +optional + Key *string `json:"key,omitempty"` +} + +// PDNSProviderSpec defines the desired state of PDNSProvider +type PDNSProviderSpec struct { + // Interval is the reconciliation interval to check the connection to the PowerDNS API + // +kubebuilder:default:="5m" + // +optional + Interval *metav1.Duration `json:"interval,omitempty"` + + // URL is the URL of the PowerDNS API + // +kubebuilder:validation:Required + // +kubebuilder:validation:Pattern=`^https?://.*` + URL string `json:"url"` + + // Vhost is the vhost/server ID of the PowerDNS API, defaults to "localhost" + // +kubebuilder:default:="localhost" + // +optional + Vhost *string `json:"vhost,omitempty"` + + // Timeout is the timeout for PowerDNS API requests, defaults to 10s + // +kubebuilder:default:="10s" + // +optional + Timeout *metav1.Duration `json:"timeout,omitempty"` + + // Proxy is the URL of the HTTP/HTTPS proxy to use for connecting to PowerDNS API + // Format: http://proxy.example.com:8080 or https://proxy.example.com:8080 + // +optional + Proxy *string `json:"proxy,omitempty"` + + // TLS defines TLS configuration for PowerDNS API connection + // +optional + TLS *PDNSProviderTLSConfig `json:"tls,omitempty"` + + // Credentials defines credentials configuration for PowerDNS API + // +kubebuilder:validation:Required + Credentials PDNSProviderCredentials `json:"credentials"` +} + +// PDNSProviderStatus defines the observed state of PDNSProvider +type PDNSProviderStatus struct { + // ConnectionStatus indicates the status of the connection to the PowerDNS API + // +optional + ConnectionStatus *string `json:"connectionStatus,omitempty"` + + // PowerDNSVersion is the version of the connected PowerDNS server + // +optional + PowerDNSVersion *string `json:"powerDNSVersion,omitempty"` + + // DaemonType is the type of PowerDNS daemon (should be "authoritative") + // +optional + DaemonType *string `json:"daemonType,omitempty"` + + // ServerID is the ID of the PowerDNS server + // +optional + ServerID *string `json:"serverID,omitempty"` + + // LastConnectionTime is the last time a successful connection was established + // +optional + LastConnectionTime *metav1.Time `json:"lastConnectionTime,omitempty"` + + // Conditions represent the latest available observations of the PDNSProvider's state + // +optional + Conditions []metav1.Condition `json:"conditions,omitempty"` + + // ObservedGeneration is the most recent generation observed for this PDNSProvider + // +optional + ObservedGeneration *int64 `json:"observedGeneration,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Cluster + +// +kubebuilder:printcolumn:name="URL",type="string",JSONPath=".spec.url" +// +kubebuilder:printcolumn:name="Connection Status",type="string",JSONPath=".status.connectionStatus" +// +kubebuilder:printcolumn:name="PowerDNS Version",type="string",JSONPath=".status.powerDNSVersion" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" +// PDNSProvider is the Schema for the pdnsproviders API +type PDNSProvider struct { + metav1.TypeMeta `json:",inline"` + + // metadata is a standard object metadata + // +optional + metav1.ObjectMeta `json:"metadata,omitempty,omitzero"` + + // spec defines the desired state of PDNSProvider + // +required + Spec PDNSProviderSpec `json:"spec"` + + // status defines the observed state of PDNSProvider + // +optional + Status PDNSProviderStatus `json:"status,omitempty,omitzero"` +} + +// +kubebuilder:object:root=true + +// PDNSProviderList contains a list of PDNSProvider +type PDNSProviderList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []PDNSProvider `json:"items"` +} + +func init() { + SchemeBuilder.Register(&PDNSProvider{}, &PDNSProviderList{}) +} + +// IsConnectionHealthy returns true if the PDNSProvider has a healthy connection to PowerDNS +func (c *PDNSProvider) IsConnectionHealthy() bool { + return c.Status.ConnectionStatus != nil && *c.Status.ConnectionStatus == "Connected" +} + +// GetVhost returns the API vhost, defaulting to "localhost" if not specified +func (c *PDNSProvider) GetVhost() string { + if c.Spec.Vhost != nil { + return *c.Spec.Vhost + } + return "localhost" +} + +// GetTimeout returns the API timeout, defaulting to 10 seconds if not specified +func (c *PDNSProvider) GetTimeout() time.Duration { + if c.Spec.Timeout != nil { + return c.Spec.Timeout.Duration + } + return 10 * time.Second +} + +// GetInterval returns the reconciliation interval, defaulting to 5 minutes if not specified +func (c *PDNSProvider) GetInterval() time.Duration { + if c.Spec.Interval != nil { + return c.Spec.Interval.Duration + } + return 5 * time.Minute +} + +// GetTLSInsecure returns the TLS insecure setting, defaulting to false if not specified +func (c *PDNSProvider) GetTLSInsecure() bool { + if c.Spec.TLS != nil && c.Spec.TLS.Insecure != nil { + return *c.Spec.TLS.Insecure + } + return false +} + +// GetCredentialsSecretName returns the credentials secret name +func (c *PDNSProvider) GetCredentialsSecretName() string { + return c.Spec.Credentials.SecretRef.Name +} + +// GetCredentialsSecretNamespace returns the credentials secret namespace, defaulting to cluster namespace if not specified +func (c *PDNSProvider) GetCredentialsSecretNamespace() string { + if c.Spec.Credentials.SecretRef.Namespace != nil { + return *c.Spec.Credentials.SecretRef.Namespace + } + return c.Namespace +} + +// GetCredentialsSecretKey returns the credentials secret key, defaulting to "apiKey" if not specified +func (c *PDNSProvider) GetCredentialsSecretKey() string { + if c.Spec.Credentials.SecretRef.Key != nil { + return *c.Spec.Credentials.SecretRef.Key + } + return "apiKey" +} + +// GetCABundleRefKind returns the CA bundle reference kind, defaulting to "ConfigMap" if not specified +func (c *PDNSProvider) GetCABundleRefKind() string { + if c.Spec.TLS != nil && c.Spec.TLS.CABundleRef != nil && c.Spec.TLS.CABundleRef.Kind != nil { + return *c.Spec.TLS.CABundleRef.Kind + } + return "ConfigMap" +} + +// GetCABundleRefKey returns the CA bundle reference key, defaulting to "ca.crt" if not specified +func (c *PDNSProvider) GetCABundleRefKey() string { + if c.Spec.TLS != nil && c.Spec.TLS.CABundleRef != nil && c.Spec.TLS.CABundleRef.Key != nil { + return *c.Spec.TLS.CABundleRef.Key + } + return "ca.crt" +} + +// GetCABundleRefNamespace returns the CA bundle reference namespace, defaulting to cluster namespace if not specified +func (c *PDNSProvider) GetCABundleRefNamespace() string { + if c.Spec.TLS != nil && c.Spec.TLS.CABundleRef != nil && c.Spec.TLS.CABundleRef.Namespace != nil { + return *c.Spec.TLS.CABundleRef.Namespace + } + return c.Namespace +} diff --git a/api/v1alpha3/rrset_types.go b/api/v1alpha3/rrset_types.go new file mode 100644 index 0000000..4d6612d --- /dev/null +++ b/api/v1alpha3/rrset_types.go @@ -0,0 +1,93 @@ +/* + * Software Name : PowerDNS-Operator + * + * SPDX-FileCopyrightText: Copyright (c) PowerDNS-Operator contributors + * SPDX-FileCopyrightText: Copyright (c) 2025 Orange Business Services SA + * SPDX-License-Identifier: Apache-2.0 + * + * This software is distributed under the Apache 2.0 License, + * see the "LICENSE" file for more details + */ + +package v1alpha3 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// RRsetSpec defines the desired state of RRset +type RRsetSpec struct { + // Type of the record (e.g. "A", "PTR", "MX"). + Type string `json:"type"` + // Name of the record + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" + Name string `json:"name"` + // DNS TTL of the records, in seconds. + TTL uint32 `json:"ttl"` + // All records in this Resource Record Set. + Records []string `json:"records"` + // Comment on RRSet. + // +optional + Comment *string `json:"comment,omitempty"` + // ZoneRef reference the zone the RRSet depends on. + ZoneRef ZoneRef `json:"zoneRef"` +} + +type ZoneRef struct { + // Name of the zone. + Name string `json:"name"` + // Kind of the Zone resource (Zone or ClusterZone) + // +kubebuilder:validation:Enum:=Zone;ClusterZone + Kind string `json:"kind"` +} + +// RRsetStatus defines the observed state of RRset +type RRsetStatus struct { + LastUpdateTime *metav1.Time `json:"lastUpdateTime,omitempty"` + DnsEntryName *string `json:"dnsEntryName,omitempty"` + SyncStatus *string `json:"syncStatus,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` + ObservedGeneration *int64 `json:"observedGeneration,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:storageversion +// +kubebuilder:conversion:hub +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Namespaced + +// +kubebuilder:printcolumn:name="Zone",type="string",JSONPath=".spec.zoneRef.name" +// +kubebuilder:printcolumn:name="Name",type="string",JSONPath=".status.dnsEntryName" +// +kubebuilder:printcolumn:name="Type",type="string",JSONPath=".spec.type" +// +kubebuilder:printcolumn:name="TTL",type="integer",JSONPath=".spec.ttl" +// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.syncStatus" +// +kubebuilder:printcolumn:name="Records",type="string",JSONPath=".spec.records" +// RRset is the Schema for the rrsets API +type RRset struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec RRsetSpec `json:"spec,omitempty"` + Status RRsetStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// RRsetList contains a list of RRset +type RRsetList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []RRset `json:"items"` +} + +func init() { + SchemeBuilder.Register(&RRset{}, &RRsetList{}) +} + +// IsInExpectedStatus returns true if Status.SyncStatus and Status.ObservedGeneration are, at least, at expected value +func (r *RRset) IsInExpectedStatus(expectedMinimumObservedGeneration int64, expectedSyncStatus string) bool { + return r.Status.ObservedGeneration != nil && + *r.Status.ObservedGeneration >= expectedMinimumObservedGeneration && + r.Status.SyncStatus != nil && + *r.Status.SyncStatus == expectedSyncStatus +} diff --git a/api/v1alpha3/zone_types.go b/api/v1alpha3/zone_types.go new file mode 100644 index 0000000..0f08861 --- /dev/null +++ b/api/v1alpha3/zone_types.go @@ -0,0 +1,112 @@ +/* + * Software Name : PowerDNS-Operator + * + * SPDX-FileCopyrightText: Copyright (c) PowerDNS-Operator contributors + * SPDX-FileCopyrightText: Copyright (c) 2025 Orange Business Services SA + * SPDX-License-Identifier: Apache-2.0 + * + * This software is distributed under the Apache 2.0 License, + * see the "LICENSE" file for more details + */ + +package v1alpha3 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// ZoneSpec defines the desired state of Zone +type ZoneSpec struct { + // ProviderRef is a reference to the PDNSProvider resource that manages the PowerDNS instance + // +kubebuilder:validation:Required + ProviderRef string `json:"providerRef"` + + // Kind of the zone, one of "Native", "Master", "Slave", "Producer", "Consumer". + // +kubebuilder:validation:Enum:=Native;Master;Slave;Producer;Consumer + Kind string `json:"kind"` + // List of the nameservers of the zone. + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:items:Pattern=`^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+$` + Nameservers []string `json:"nameservers"` + // The catalog this zone is a member of + // +optional + Catalog *string `json:"catalog,omitempty"` + // The SOA-EDIT-API metadata item, one of "DEFAULT", "INCREASE", "EPOCH", defaults to "DEFAULT" + // +kubebuilder:validation:Enum:=DEFAULT;INCREASE;EPOCH + // +kubebuilder:default:="DEFAULT" + // +optional + SOAEditAPI *string `json:"soa_edit_api,omitempty"` +} + +// ZoneStatus defines the observed state of Zone +type ZoneStatus struct { + // ID define the opaque zone id. + // +optional + ID *string `json:"id,omitempty"` + // Name of the zone (e.g. "example.com.") + // +optional + Name *string `json:"name,omitempty"` + // Kind of the zone, one of "Native", "Master", "Slave", "Producer", "Consumer". + // +optional + Kind *string `json:"kind,omitempty"` + // The SOA serial number. + // +optional + Serial *uint32 `json:"serial,omitempty"` + // The SOA serial notifications have been sent out for + // +optional + NotifiedSerial *uint32 `json:"notified_serial,omitempty"` + // The SOA serial as seen in query responses. + // +optional + EditedSerial *uint32 `json:"edited_serial,omitempty"` + // List of IP addresses configured as a master for this zone ("Slave" type zones only). + // +optional + Masters []string `json:"masters,omitempty"` + // Whether or not this zone is DNSSEC signed. + // +optional + DNSsec *bool `json:"dnssec,omitempty"` + // The catalog this zone is a member of. + // +optional + Catalog *string `json:"catalog,omitempty"` + SyncStatus *string `json:"syncStatus,omitempty"` + Conditions []metav1.Condition `json:"conditions,omitempty"` + ObservedGeneration *int64 `json:"observedGeneration,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:storageversion +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Namespaced + +// +kubebuilder:printcolumn:name="Provider",type="string",JSONPath=".spec.providerRef" +// +kubebuilder:printcolumn:name="Serial",type="integer",JSONPath=".status.serial" +// +kubebuilder:printcolumn:name="ID",type="string",JSONPath=".status.id" +// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.syncStatus" +// Zone is the Schema for the zones API +type Zone struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ZoneSpec `json:"spec,omitempty"` + Status ZoneStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// ZoneList contains a list of Zone +type ZoneList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Zone `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Zone{}, &ZoneList{}) +} + +// IsInExpectedStatus returns true if Status.SyncStatus and Status.ObservedGeneration are, at least, at expected value +func (z *Zone) IsInExpectedStatus(expectedMinimumObservedGeneration int64, expectedSyncStatus string) bool { + return z.Status.ObservedGeneration != nil && + *z.Status.ObservedGeneration >= expectedMinimumObservedGeneration && + z.Status.SyncStatus != nil && + *z.Status.SyncStatus == expectedSyncStatus +} diff --git a/api/v1alpha3/zz_generated.deepcopy.go b/api/v1alpha3/zz_generated.deepcopy.go new file mode 100644 index 0000000..1bdea68 --- /dev/null +++ b/api/v1alpha3/zz_generated.deepcopy.go @@ -0,0 +1,693 @@ +//go:build !ignore_autogenerated + +/* + * Software Name : PowerDNS-Operator + * + * SPDX-FileCopyrightText: Copyright (c) PowerDNS-Operator contributors + * SPDX-FileCopyrightText: Copyright (c) 2025 Orange Business Services SA + * SPDX-License-Identifier: Apache-2.0 + * + * This software is distributed under the Apache 2.0 License, + * see the "LICENSE" file for more details + */ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha3 + +import ( + "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterRRset) DeepCopyInto(out *ClusterRRset) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterRRset. +func (in *ClusterRRset) DeepCopy() *ClusterRRset { + if in == nil { + return nil + } + out := new(ClusterRRset) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterRRset) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterRRsetList) DeepCopyInto(out *ClusterRRsetList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ClusterRRset, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterRRsetList. +func (in *ClusterRRsetList) DeepCopy() *ClusterRRsetList { + if in == nil { + return nil + } + out := new(ClusterRRsetList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterRRsetList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterZone) DeepCopyInto(out *ClusterZone) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterZone. +func (in *ClusterZone) DeepCopy() *ClusterZone { + if in == nil { + return nil + } + out := new(ClusterZone) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterZone) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterZoneList) DeepCopyInto(out *ClusterZoneList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ClusterZone, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterZoneList. +func (in *ClusterZoneList) DeepCopy() *ClusterZoneList { + if in == nil { + return nil + } + out := new(ClusterZoneList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterZoneList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PDNSProvider) DeepCopyInto(out *PDNSProvider) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PDNSProvider. +func (in *PDNSProvider) DeepCopy() *PDNSProvider { + if in == nil { + return nil + } + out := new(PDNSProvider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PDNSProvider) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PDNSProviderCABundleRef) DeepCopyInto(out *PDNSProviderCABundleRef) { + *out = *in + if in.Namespace != nil { + in, out := &in.Namespace, &out.Namespace + *out = new(string) + **out = **in + } + if in.Kind != nil { + in, out := &in.Kind, &out.Kind + *out = new(string) + **out = **in + } + if in.Key != nil { + in, out := &in.Key, &out.Key + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PDNSProviderCABundleRef. +func (in *PDNSProviderCABundleRef) DeepCopy() *PDNSProviderCABundleRef { + if in == nil { + return nil + } + out := new(PDNSProviderCABundleRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PDNSProviderCredentials) DeepCopyInto(out *PDNSProviderCredentials) { + *out = *in + in.SecretRef.DeepCopyInto(&out.SecretRef) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PDNSProviderCredentials. +func (in *PDNSProviderCredentials) DeepCopy() *PDNSProviderCredentials { + if in == nil { + return nil + } + out := new(PDNSProviderCredentials) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PDNSProviderList) DeepCopyInto(out *PDNSProviderList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]PDNSProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PDNSProviderList. +func (in *PDNSProviderList) DeepCopy() *PDNSProviderList { + if in == nil { + return nil + } + out := new(PDNSProviderList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PDNSProviderList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PDNSProviderSecretRef) DeepCopyInto(out *PDNSProviderSecretRef) { + *out = *in + if in.Namespace != nil { + in, out := &in.Namespace, &out.Namespace + *out = new(string) + **out = **in + } + if in.Key != nil { + in, out := &in.Key, &out.Key + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PDNSProviderSecretRef. +func (in *PDNSProviderSecretRef) DeepCopy() *PDNSProviderSecretRef { + if in == nil { + return nil + } + out := new(PDNSProviderSecretRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PDNSProviderSpec) DeepCopyInto(out *PDNSProviderSpec) { + *out = *in + if in.Interval != nil { + in, out := &in.Interval, &out.Interval + *out = new(v1.Duration) + **out = **in + } + if in.Vhost != nil { + in, out := &in.Vhost, &out.Vhost + *out = new(string) + **out = **in + } + if in.Timeout != nil { + in, out := &in.Timeout, &out.Timeout + *out = new(v1.Duration) + **out = **in + } + if in.Proxy != nil { + in, out := &in.Proxy, &out.Proxy + *out = new(string) + **out = **in + } + if in.TLS != nil { + in, out := &in.TLS, &out.TLS + *out = new(PDNSProviderTLSConfig) + (*in).DeepCopyInto(*out) + } + in.Credentials.DeepCopyInto(&out.Credentials) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PDNSProviderSpec. +func (in *PDNSProviderSpec) DeepCopy() *PDNSProviderSpec { + if in == nil { + return nil + } + out := new(PDNSProviderSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PDNSProviderStatus) DeepCopyInto(out *PDNSProviderStatus) { + *out = *in + if in.ConnectionStatus != nil { + in, out := &in.ConnectionStatus, &out.ConnectionStatus + *out = new(string) + **out = **in + } + if in.PowerDNSVersion != nil { + in, out := &in.PowerDNSVersion, &out.PowerDNSVersion + *out = new(string) + **out = **in + } + if in.DaemonType != nil { + in, out := &in.DaemonType, &out.DaemonType + *out = new(string) + **out = **in + } + if in.ServerID != nil { + in, out := &in.ServerID, &out.ServerID + *out = new(string) + **out = **in + } + if in.LastConnectionTime != nil { + in, out := &in.LastConnectionTime, &out.LastConnectionTime + *out = (*in).DeepCopy() + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ObservedGeneration != nil { + in, out := &in.ObservedGeneration, &out.ObservedGeneration + *out = new(int64) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PDNSProviderStatus. +func (in *PDNSProviderStatus) DeepCopy() *PDNSProviderStatus { + if in == nil { + return nil + } + out := new(PDNSProviderStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PDNSProviderTLSConfig) DeepCopyInto(out *PDNSProviderTLSConfig) { + *out = *in + if in.Insecure != nil { + in, out := &in.Insecure, &out.Insecure + *out = new(bool) + **out = **in + } + if in.CABundleRef != nil { + in, out := &in.CABundleRef, &out.CABundleRef + *out = new(PDNSProviderCABundleRef) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PDNSProviderTLSConfig. +func (in *PDNSProviderTLSConfig) DeepCopy() *PDNSProviderTLSConfig { + if in == nil { + return nil + } + out := new(PDNSProviderTLSConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RRset) DeepCopyInto(out *RRset) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RRset. +func (in *RRset) DeepCopy() *RRset { + if in == nil { + return nil + } + out := new(RRset) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RRset) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RRsetList) DeepCopyInto(out *RRsetList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]RRset, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RRsetList. +func (in *RRsetList) DeepCopy() *RRsetList { + if in == nil { + return nil + } + out := new(RRsetList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RRsetList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RRsetSpec) DeepCopyInto(out *RRsetSpec) { + *out = *in + if in.Records != nil { + in, out := &in.Records, &out.Records + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Comment != nil { + in, out := &in.Comment, &out.Comment + *out = new(string) + **out = **in + } + out.ZoneRef = in.ZoneRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RRsetSpec. +func (in *RRsetSpec) DeepCopy() *RRsetSpec { + if in == nil { + return nil + } + out := new(RRsetSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RRsetStatus) DeepCopyInto(out *RRsetStatus) { + *out = *in + if in.LastUpdateTime != nil { + in, out := &in.LastUpdateTime, &out.LastUpdateTime + *out = (*in).DeepCopy() + } + if in.DnsEntryName != nil { + in, out := &in.DnsEntryName, &out.DnsEntryName + *out = new(string) + **out = **in + } + if in.SyncStatus != nil { + in, out := &in.SyncStatus, &out.SyncStatus + *out = new(string) + **out = **in + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ObservedGeneration != nil { + in, out := &in.ObservedGeneration, &out.ObservedGeneration + *out = new(int64) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RRsetStatus. +func (in *RRsetStatus) DeepCopy() *RRsetStatus { + if in == nil { + return nil + } + out := new(RRsetStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Zone) DeepCopyInto(out *Zone) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Zone. +func (in *Zone) DeepCopy() *Zone { + if in == nil { + return nil + } + out := new(Zone) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Zone) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ZoneList) DeepCopyInto(out *ZoneList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Zone, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ZoneList. +func (in *ZoneList) DeepCopy() *ZoneList { + if in == nil { + return nil + } + out := new(ZoneList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ZoneList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ZoneRef) DeepCopyInto(out *ZoneRef) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ZoneRef. +func (in *ZoneRef) DeepCopy() *ZoneRef { + if in == nil { + return nil + } + out := new(ZoneRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ZoneSpec) DeepCopyInto(out *ZoneSpec) { + *out = *in + if in.Nameservers != nil { + in, out := &in.Nameservers, &out.Nameservers + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Catalog != nil { + in, out := &in.Catalog, &out.Catalog + *out = new(string) + **out = **in + } + if in.SOAEditAPI != nil { + in, out := &in.SOAEditAPI, &out.SOAEditAPI + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ZoneSpec. +func (in *ZoneSpec) DeepCopy() *ZoneSpec { + if in == nil { + return nil + } + out := new(ZoneSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ZoneStatus) DeepCopyInto(out *ZoneStatus) { + *out = *in + if in.ID != nil { + in, out := &in.ID, &out.ID + *out = new(string) + **out = **in + } + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = new(string) + **out = **in + } + if in.Kind != nil { + in, out := &in.Kind, &out.Kind + *out = new(string) + **out = **in + } + if in.Serial != nil { + in, out := &in.Serial, &out.Serial + *out = new(uint32) + **out = **in + } + if in.NotifiedSerial != nil { + in, out := &in.NotifiedSerial, &out.NotifiedSerial + *out = new(uint32) + **out = **in + } + if in.EditedSerial != nil { + in, out := &in.EditedSerial, &out.EditedSerial + *out = new(uint32) + **out = **in + } + if in.Masters != nil { + in, out := &in.Masters, &out.Masters + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.DNSsec != nil { + in, out := &in.DNSsec, &out.DNSsec + *out = new(bool) + **out = **in + } + if in.Catalog != nil { + in, out := &in.Catalog, &out.Catalog + *out = new(string) + **out = **in + } + if in.SyncStatus != nil { + in, out := &in.SyncStatus, &out.SyncStatus + *out = new(string) + **out = **in + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ObservedGeneration != nil { + in, out := &in.ObservedGeneration, &out.ObservedGeneration + *out = new(int64) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ZoneStatus. +func (in *ZoneStatus) DeepCopy() *ZoneStatus { + if in == nil { + return nil + } + out := new(ZoneStatus) + in.DeepCopyInto(out) + return out +} diff --git a/cmd/main.go b/cmd/main.go index 5371246..5149456 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -12,14 +12,9 @@ package main import ( - "context" "crypto/tls" - "crypto/x509" "flag" - "net/http" "os" - "strconv" - "time" // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. @@ -36,10 +31,9 @@ import ( "github.com/powerdns-operator/powerdns-operator/internal/controller" - powerdns "github.com/joeig/go-powerdns/v3" - dnsv1alpha2 "github.com/powerdns-operator/powerdns-operator/api/v1alpha2" - //+kubebuilder:scaffold:imports + dnsv1alpha3 "github.com/powerdns-operator/powerdns-operator/api/v1alpha3" + // +kubebuilder:scaffold:imports ) var ( @@ -51,7 +45,8 @@ func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(dnsv1alpha2.AddToScheme(scheme)) - //+kubebuilder:scaffold:scheme + utilruntime.Must(dnsv1alpha3.AddToScheme(scheme)) + // +kubebuilder:scaffold:scheme } func main() { @@ -61,31 +56,6 @@ func main() { var secureMetrics bool var enableHTTP2 bool - // Get environment variables for PowerDNS API configuration - apiURL := os.Getenv("PDNS_API_URL") - apiKey := os.Getenv("PDNS_API_KEY") - apiVhost := os.Getenv("PDNS_API_VHOST") - if apiVhost == "" { - apiVhost = "localhost" - } - apiInsecureStr := os.Getenv("PDNS_API_INSECURE") - var apiInsecure bool - if apiInsecureStr != "" { - if insecure, err := strconv.ParseBool(apiInsecureStr); err == nil { - apiInsecure = insecure - } - } - apiCAPath := os.Getenv("PDNS_API_CA_PATH") - - // Parse PowerDNS API timeout from environment variable (in seconds) - apiTimeoutStr := os.Getenv("PDNS_API_TIMEOUT") - apiTimeoutSeconds := 10 // default timeout in seconds - if apiTimeoutStr != "" { - if timeout, err := strconv.Atoi(apiTimeoutStr); err == nil && timeout > 0 { - apiTimeoutSeconds = timeout - } - } - // Parse command line flags flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") @@ -96,14 +66,6 @@ func main() { "If set the metrics endpoint is served securely") flag.BoolVar(&enableHTTP2, "enable-http2", false, "If set, HTTP/2 will be enabled for the metrics and webhook servers") - flag.StringVar(&apiURL, "pdns-api-url", apiURL, "The URL of the PowerDNS API") - flag.StringVar(&apiKey, "pdns-api-key", apiKey, "The API key to authenticate with the PowerDNS API") - flag.StringVar(&apiVhost, "pdns-api-vhost", apiVhost, "The vhost of the PowerDNS API") - flag.IntVar(&apiTimeoutSeconds, "pdns-api-timeout", apiTimeoutSeconds, - "The timeout for PowerDNS API requests (in seconds)") - flag.BoolVar(&apiInsecure, "pdns-api-insecure", apiInsecure, - "Enable insecure connections to PowerDNS API") - flag.StringVar(&apiCAPath, "pdns-api-ca-path", apiCAPath, "The path to certificate authority") opts := zap.Options{ Development: false, @@ -113,18 +75,7 @@ func main() { ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) - // Validate mandatory configuration - if apiURL == "" { - setupLog.Error(nil, "PDNS_API_URL environment variable or --pdns-api-url flag is required") - os.Exit(1) - } - setupLog.Info("PowerDNS API URL", "url", apiURL) - - if apiKey == "" { - setupLog.Error(nil, "PDNS_API_KEY environment variable or --pdns-api-key flag is required") - os.Exit(1) - } - setupLog.Info("PowerDNS API vhost", "vhost", apiVhost) + setupLog.Info("PowerDNS configuration will be loaded from PDNSProvider resources") // if the enable-http2 flag is false (the default), http/2 should be disabled // due to its vulnerabilities. More specifically, disabling http/2 will @@ -174,47 +125,9 @@ func main() { os.Exit(1) } - // Initialize a http.Client to communicate with PowerDNS API - var httpClient *http.Client - tlsConfig := &tls.Config{ - InsecureSkipVerify: apiInsecure, - } - if apiInsecure { - setupLog.Info("the communication with PowerDNS API is set as insecure") - } - - if apiCAPath != "" { - caCert, err := os.ReadFile(apiCAPath) - if err != nil { - setupLog.Error(err, "unable to load CA certificate") - os.Exit(1) - } - caCertPool := x509.NewCertPool() - ok := caCertPool.AppendCertsFromPEM(caCert) - if !ok { - setupLog.Error(err, "unable to parse CA certificate") - os.Exit(1) - } - setupLog.Info("CA certificate parsed successfully", "apiCAPath", apiCAPath) - tlsConfig.RootCAs = caCertPool - } - - tr := &http.Transport{TLSClientConfig: tlsConfig} - httpClient = &http.Client{Transport: tr} - - pdnsClient, err := PDNSClientInitializer(apiURL, apiKey, apiVhost, apiTimeoutSeconds, - httpClient) - if err != nil { - setupLog.Error(err, "unable to initialize connection with PowerDNS server") - os.Exit(1) - } if err = (&controller.ZoneReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), - PDNSClient: controller.PdnsClienter{ - Records: pdnsClient.Records, - Zones: pdnsClient.Zones, - }, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Zone") os.Exit(1) @@ -222,10 +135,6 @@ func main() { if err = (&controller.RRsetReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), - PDNSClient: controller.PdnsClienter{ - Records: pdnsClient.Records, - Zones: pdnsClient.Zones, - }, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "RRset") os.Exit(1) @@ -233,10 +142,6 @@ func main() { if err = (&controller.ClusterZoneReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), - PDNSClient: controller.PdnsClienter{ - Records: pdnsClient.Records, - Zones: pdnsClient.Zones, - }, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "ClusterZone") os.Exit(1) @@ -244,15 +149,18 @@ func main() { if err = (&controller.ClusterRRsetReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), - PDNSClient: controller.PdnsClienter{ - Records: pdnsClient.Records, - Zones: pdnsClient.Zones, - }, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "ClusterRRset") os.Exit(1) } - //+kubebuilder:scaffold:builder + if err := (&controller.PDNSProviderReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "PDNSProvider") + os.Exit(1) + } + // +kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { setupLog.Error(err, "unable to set up health check") @@ -269,37 +177,3 @@ func main() { os.Exit(1) } } - -func PDNSClientInitializer(baseURL string, key string, vhost string, timeoutSeconds int, - httpClient *http.Client) (*powerdns.Client, error) { - client := powerdns.New(baseURL, vhost, powerdns.WithAPIKey(key), powerdns.WithHTTPClient(httpClient)) - - // Test connectivity by attempting to get server information - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSeconds)*time.Second) - defer cancel() - - server, err := client.Servers.Get(ctx, vhost) - if err != nil { - return nil, err - } - - // Log server information for operational visibility - if server.Version != nil { - setupLog.Info("Connected to PowerDNS server", "version", *server.Version) - } - if server.DaemonType != nil { - setupLog.Info("PowerDNS daemon type", "type", *server.DaemonType) - // Validate that we're connecting to an Authoritative server (not Recursor) - if *server.DaemonType != "authoritative" { - setupLog.Info("Warning: PowerDNS Operator is designed for Authoritative servers", "daemon_type", *server.DaemonType) - } - } - if server.ID != nil { - setupLog.Info("PowerDNS server ID", "id", *server.ID) - } - - // Log successful connection with key details - setupLog.Info("PowerDNS connectivity test successful", "url", baseURL, "vhost", vhost) - - return client, nil -} diff --git a/config/crd/bases/dns.cav.enablers.ob_clusterrrsets.yaml b/config/crd/bases/dns.cav.enablers.ob_clusterrrsets.yaml index 995fa1f..18d55bf 100644 --- a/config/crd/bases/dns.cav.enablers.ob_clusterrrsets.yaml +++ b/config/crd/bases/dns.cav.enablers.ob_clusterrrsets.yaml @@ -174,6 +174,169 @@ spec: type: object type: object served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .spec.zoneRef.name + name: Zone + type: string + - jsonPath: .status.dnsEntryName + name: Name + type: string + - jsonPath: .spec.type + name: Type + type: string + - jsonPath: .spec.ttl + name: TTL + type: integer + - jsonPath: .status.syncStatus + name: Status + type: string + - jsonPath: .spec.records + name: Records + type: string + name: v1alpha3 + schema: + openAPIV3Schema: + description: ClusterRRset is the Schema for the clusterrrsets API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: RRsetSpec defines the desired state of RRset + properties: + comment: + description: Comment on RRSet. + type: string + name: + description: Name of the record + type: string + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf + records: + description: All records in this Resource Record Set. + items: + type: string + type: array + ttl: + description: DNS TTL of the records, in seconds. + format: int32 + type: integer + type: + description: Type of the record (e.g. "A", "PTR", "MX"). + type: string + zoneRef: + description: ZoneRef reference the zone the RRSet depends on. + properties: + kind: + description: Kind of the Zone resource (Zone or ClusterZone) + enum: + - Zone + - ClusterZone + type: string + name: + description: Name of the zone. + type: string + required: + - kind + - name + type: object + required: + - name + - records + - ttl + - type + - zoneRef + type: object + status: + description: RRsetStatus defines the observed state of RRset + properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + dnsEntryName: + type: string + lastUpdateTime: + format: date-time + type: string + observedGeneration: + format: int64 + type: integer + syncStatus: + type: string + type: object + type: object + served: true storage: true subresources: status: {} diff --git a/config/crd/bases/dns.cav.enablers.ob_clusterzones.yaml b/config/crd/bases/dns.cav.enablers.ob_clusterzones.yaml index 24cdc49..d15ec51 100644 --- a/config/crd/bases/dns.cav.enablers.ob_clusterzones.yaml +++ b/config/crd/bases/dns.cav.enablers.ob_clusterzones.yaml @@ -183,6 +183,186 @@ spec: type: object type: object served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .spec.providerRef + name: PDNSProvider + type: string + - jsonPath: .status.serial + name: Serial + type: integer + - jsonPath: .status.id + name: ID + type: string + - jsonPath: .status.syncStatus + name: Status + type: string + name: v1alpha3 + schema: + openAPIV3Schema: + description: ClusterZone is the Schema for the clusterzones API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: ZoneSpec defines the desired state of Zone + properties: + catalog: + description: The catalog this zone is a member of + type: string + kind: + description: Kind of the zone, one of "Native", "Master", "Slave", + "Producer", "Consumer". + enum: + - Native + - Master + - Slave + - Producer + - Consumer + type: string + nameservers: + description: List of the nameservers of the zone. + items: + pattern: ^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+$ + type: string + minItems: 1 + type: array + providerRef: + description: ProviderRef is a reference to the PDNSProvider resource + that manages the PowerDNS instance + type: string + soa_edit_api: + default: DEFAULT + description: The SOA-EDIT-API metadata item, one of "DEFAULT", "INCREASE", + "EPOCH", defaults to "DEFAULT" + enum: + - DEFAULT + - INCREASE + - EPOCH + type: string + required: + - kind + - nameservers + - providerRef + type: object + status: + description: ZoneStatus defines the observed state of Zone + properties: + catalog: + description: The catalog this zone is a member of. + type: string + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + dnssec: + description: Whether or not this zone is DNSSEC signed. + type: boolean + edited_serial: + description: The SOA serial as seen in query responses. + format: int32 + type: integer + id: + description: ID define the opaque zone id. + type: string + kind: + description: Kind of the zone, one of "Native", "Master", "Slave", + "Producer", "Consumer". + type: string + masters: + description: List of IP addresses configured as a master for this + zone ("Slave" type zones only). + items: + type: string + type: array + name: + description: Name of the zone (e.g. "example.com.") + type: string + notified_serial: + description: The SOA serial notifications have been sent out for + format: int32 + type: integer + observedGeneration: + format: int64 + type: integer + serial: + description: The SOA serial number. + format: int32 + type: integer + syncStatus: + type: string + type: object + type: object + served: true storage: true subresources: status: {} diff --git a/config/crd/bases/dns.cav.enablers.ob_pdnsproviders.yaml b/config/crd/bases/dns.cav.enablers.ob_pdnsproviders.yaml new file mode 100644 index 0000000..b108b1b --- /dev/null +++ b/config/crd/bases/dns.cav.enablers.ob_pdnsproviders.yaml @@ -0,0 +1,239 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.18.0 + name: pdnsproviders.dns.cav.enablers.ob +spec: + group: dns.cav.enablers.ob + names: + kind: PDNSProvider + listKind: PDNSProviderList + plural: pdnsproviders + singular: pdnsprovider + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .spec.url + name: URL + type: string + - jsonPath: .status.connectionStatus + name: Connection Status + type: string + - jsonPath: .status.powerDNSVersion + name: PowerDNS Version + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha3 + schema: + openAPIV3Schema: + description: PDNSProvider is the Schema for the pdnsproviders API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: spec defines the desired state of PDNSProvider + properties: + credentials: + description: Credentials defines credentials configuration for PowerDNS + API + properties: + secretRef: + description: SecretRef is a reference to a Kubernetes Secret containing + the PowerDNS API key + properties: + key: + default: apiKey + description: Key is the key in the Secret containing the API + key + type: string + name: + description: Name is the name of the Secret + type: string + namespace: + description: |- + Namespace is the namespace of the Secret + If not specified, defaults to the PDNSProvider resource namespace + type: string + required: + - name + type: object + required: + - secretRef + type: object + interval: + default: 5m + description: Interval is the reconciliation interval to check the + connection to the PowerDNS API + type: string + proxy: + description: |- + Proxy is the URL of the HTTP/HTTPS proxy to use for connecting to PowerDNS API + Format: http://proxy.example.com:8080 or https://proxy.example.com:8080 + type: string + timeout: + default: 10s + description: Timeout is the timeout for PowerDNS API requests, defaults + to 10s + type: string + tls: + description: TLS defines TLS configuration for PowerDNS API connection + properties: + caBundleRef: + description: CABundleRef is a reference to a ConfigMap or Secret + containing a CA bundle + properties: + key: + default: ca.crt + description: Key is the key in the ConfigMap or Secret containing + the CA bundle + type: string + kind: + default: ConfigMap + description: Kind is the kind of resource (ConfigMap or Secret) + enum: + - ConfigMap + - Secret + type: string + name: + description: Name is the name of the ConfigMap or Secret + type: string + namespace: + description: |- + Namespace is the namespace of the ConfigMap or Secret + If not specified, defaults to the operator namespace + type: string + required: + - name + type: object + insecure: + default: false + description: Insecure enables insecure connections to PowerDNS + API (skip TLS verification) + type: boolean + type: object + url: + description: URL is the URL of the PowerDNS API + pattern: ^https?://.* + type: string + vhost: + default: localhost + description: Vhost is the vhost/server ID of the PowerDNS API, defaults + to "localhost" + type: string + required: + - credentials + - url + type: object + status: + description: status defines the observed state of PDNSProvider + properties: + conditions: + description: Conditions represent the latest available observations + of the PDNSProvider's state + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + connectionStatus: + description: ConnectionStatus indicates the status of the connection + to the PowerDNS API + type: string + daemonType: + description: DaemonType is the type of PowerDNS daemon (should be + "authoritative") + type: string + lastConnectionTime: + description: LastConnectionTime is the last time a successful connection + was established + format: date-time + type: string + observedGeneration: + description: ObservedGeneration is the most recent generation observed + for this PDNSProvider + format: int64 + type: integer + powerDNSVersion: + description: PowerDNSVersion is the version of the connected PowerDNS + server + type: string + serverID: + description: ServerID is the ID of the PowerDNS server + type: string + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/dns.cav.enablers.ob_rrsets.yaml b/config/crd/bases/dns.cav.enablers.ob_rrsets.yaml index 2babc46..224b2dd 100644 --- a/config/crd/bases/dns.cav.enablers.ob_rrsets.yaml +++ b/config/crd/bases/dns.cav.enablers.ob_rrsets.yaml @@ -330,6 +330,169 @@ spec: type: object type: object served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .spec.zoneRef.name + name: Zone + type: string + - jsonPath: .status.dnsEntryName + name: Name + type: string + - jsonPath: .spec.type + name: Type + type: string + - jsonPath: .spec.ttl + name: TTL + type: integer + - jsonPath: .status.syncStatus + name: Status + type: string + - jsonPath: .spec.records + name: Records + type: string + name: v1alpha3 + schema: + openAPIV3Schema: + description: RRset is the Schema for the rrsets API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: RRsetSpec defines the desired state of RRset + properties: + comment: + description: Comment on RRSet. + type: string + name: + description: Name of the record + type: string + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf + records: + description: All records in this Resource Record Set. + items: + type: string + type: array + ttl: + description: DNS TTL of the records, in seconds. + format: int32 + type: integer + type: + description: Type of the record (e.g. "A", "PTR", "MX"). + type: string + zoneRef: + description: ZoneRef reference the zone the RRSet depends on. + properties: + kind: + description: Kind of the Zone resource (Zone or ClusterZone) + enum: + - Zone + - ClusterZone + type: string + name: + description: Name of the zone. + type: string + required: + - kind + - name + type: object + required: + - name + - records + - ttl + - type + - zoneRef + type: object + status: + description: RRsetStatus defines the observed state of RRset + properties: + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + dnsEntryName: + type: string + lastUpdateTime: + format: date-time + type: string + observedGeneration: + format: int64 + type: integer + syncStatus: + type: string + type: object + type: object + served: true storage: true subresources: status: {} diff --git a/config/crd/bases/dns.cav.enablers.ob_zones.yaml b/config/crd/bases/dns.cav.enablers.ob_zones.yaml index de4e817..9ac2d88 100644 --- a/config/crd/bases/dns.cav.enablers.ob_zones.yaml +++ b/config/crd/bases/dns.cav.enablers.ob_zones.yaml @@ -355,6 +355,186 @@ spec: type: object type: object served: true + storage: false + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .spec.providerRef + name: Provider + type: string + - jsonPath: .status.serial + name: Serial + type: integer + - jsonPath: .status.id + name: ID + type: string + - jsonPath: .status.syncStatus + name: Status + type: string + name: v1alpha3 + schema: + openAPIV3Schema: + description: Zone is the Schema for the zones API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: ZoneSpec defines the desired state of Zone + properties: + catalog: + description: The catalog this zone is a member of + type: string + kind: + description: Kind of the zone, one of "Native", "Master", "Slave", + "Producer", "Consumer". + enum: + - Native + - Master + - Slave + - Producer + - Consumer + type: string + nameservers: + description: List of the nameservers of the zone. + items: + pattern: ^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+$ + type: string + minItems: 1 + type: array + providerRef: + description: ProviderRef is a reference to the PDNSProvider resource + that manages the PowerDNS instance + type: string + soa_edit_api: + default: DEFAULT + description: The SOA-EDIT-API metadata item, one of "DEFAULT", "INCREASE", + "EPOCH", defaults to "DEFAULT" + enum: + - DEFAULT + - INCREASE + - EPOCH + type: string + required: + - kind + - nameservers + - providerRef + type: object + status: + description: ZoneStatus defines the observed state of Zone + properties: + catalog: + description: The catalog this zone is a member of. + type: string + conditions: + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + dnssec: + description: Whether or not this zone is DNSSEC signed. + type: boolean + edited_serial: + description: The SOA serial as seen in query responses. + format: int32 + type: integer + id: + description: ID define the opaque zone id. + type: string + kind: + description: Kind of the zone, one of "Native", "Master", "Slave", + "Producer", "Consumer". + type: string + masters: + description: List of IP addresses configured as a master for this + zone ("Slave" type zones only). + items: + type: string + type: array + name: + description: Name of the zone (e.g. "example.com.") + type: string + notified_serial: + description: The SOA serial notifications have been sent out for + format: int32 + type: integer + observedGeneration: + format: int64 + type: integer + serial: + description: The SOA serial number. + format: int32 + type: integer + syncStatus: + type: string + type: object + type: object + served: true storage: true subresources: status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index abed201..8ba3fa7 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -6,6 +6,7 @@ resources: - bases/dns.cav.enablers.ob_rrsets.yaml - bases/dns.cav.enablers.ob_clusterzones.yaml - bases/dns.cav.enablers.ob_clusterrrsets.yaml +- bases/dns.cav.enablers.ob_pdnsproviders.yaml #+kubebuilder:scaffold:crdkustomizeresource patches: diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index 5c5f0b8..2e6cc79 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -1,2 +1,2 @@ resources: -- manager.yaml +- manager.yaml \ No newline at end of file diff --git a/config/rbac/clusterrrset_admin_role.yaml b/config/rbac/clusterrrset_admin_role.yaml new file mode 100644 index 0000000..f53ed95 --- /dev/null +++ b/config/rbac/clusterrrset_admin_role.yaml @@ -0,0 +1,27 @@ +# This rule is not used by the project powerdns-operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants full permissions ('*') over dns.cav.enablers.ob. +# This role is intended for users authorized to modify roles and bindings within the cluster, +# enabling them to delegate specific permissions to other users or groups as needed. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: powerdns-operator + app.kubernetes.io/managed-by: kustomize + name: clusterrrset-admin-role +rules: +- apiGroups: + - dns.cav.enablers.ob + resources: + - clusterrrsets + verbs: + - '*' +- apiGroups: + - dns.cav.enablers.ob + resources: + - clusterrrsets/status + verbs: + - get diff --git a/config/rbac/clusterzone_admin_role.yaml b/config/rbac/clusterzone_admin_role.yaml new file mode 100644 index 0000000..c12ac63 --- /dev/null +++ b/config/rbac/clusterzone_admin_role.yaml @@ -0,0 +1,27 @@ +# This rule is not used by the project powerdns-operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants full permissions ('*') over dns.cav.enablers.ob. +# This role is intended for users authorized to modify roles and bindings within the cluster, +# enabling them to delegate specific permissions to other users or groups as needed. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: powerdns-operator + app.kubernetes.io/managed-by: kustomize + name: clusterzone-admin-role +rules: +- apiGroups: + - dns.cav.enablers.ob + resources: + - clusterzones + verbs: + - '*' +- apiGroups: + - dns.cav.enablers.ob + resources: + - clusterzones/status + verbs: + - get diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml index 694c39d..0be9792 100644 --- a/config/rbac/kustomization.yaml +++ b/config/rbac/kustomization.yaml @@ -23,3 +23,10 @@ resources: - zone_editor_role.yaml - zone_viewer_role.yaml +# For each CRD, "Admin", "Editor" and "Viewer" roles are scaffolded by +# default, aiding admins in cluster management. Those roles are +# not used by the powerdns-operator itself. You can comment the following lines +# if you do not want those helpers be installed with your Project. +- pdnsprovider_admin_role.yaml +- pdnsprovider_editor_role.yaml +- pdnsprovider_viewer_role.yaml diff --git a/config/rbac/pdnsprovider_admin_role.yaml b/config/rbac/pdnsprovider_admin_role.yaml new file mode 100644 index 0000000..314db2e --- /dev/null +++ b/config/rbac/pdnsprovider_admin_role.yaml @@ -0,0 +1,27 @@ +# This rule is not used by the project powerdns-operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants full permissions ('*') over dns.cav.enablers.ob. +# This role is intended for users authorized to modify roles and bindings within the cluster, +# enabling them to delegate specific permissions to other users or groups as needed. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: powerdns-operator + app.kubernetes.io/managed-by: kustomize + name: pdnsprovider-admin-role +rules: +- apiGroups: + - dns.cav.enablers.ob + resources: + - pdnsproviders + verbs: + - '*' +- apiGroups: + - dns.cav.enablers.ob + resources: + - pdnsproviders/status + verbs: + - get diff --git a/config/rbac/pdnsprovider_editor_role.yaml b/config/rbac/pdnsprovider_editor_role.yaml new file mode 100644 index 0000000..aa03702 --- /dev/null +++ b/config/rbac/pdnsprovider_editor_role.yaml @@ -0,0 +1,33 @@ +# This rule is not used by the project powerdns-operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants permissions to create, update, and delete resources within the dns.cav.enablers.ob. +# This role is intended for users who need to manage these resources +# but should not control RBAC or manage permissions for others. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: powerdns-operator + app.kubernetes.io/managed-by: kustomize + name: pdnsprovider-editor-role +rules: +- apiGroups: + - dns.cav.enablers.ob + resources: + - pdnsproviders + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - dns.cav.enablers.ob + resources: + - pdnsproviders/status + verbs: + - get diff --git a/config/rbac/pdnsprovider_viewer_role.yaml b/config/rbac/pdnsprovider_viewer_role.yaml new file mode 100644 index 0000000..c64749e --- /dev/null +++ b/config/rbac/pdnsprovider_viewer_role.yaml @@ -0,0 +1,29 @@ +# This rule is not used by the project powerdns-operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants read-only access to dns.cav.enablers.ob resources. +# This role is intended for users who need visibility into these resources +# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: powerdns-operator + app.kubernetes.io/managed-by: kustomize + name: pdnsprovider-viewer-role +rules: +- apiGroups: + - dns.cav.enablers.ob + resources: + - pdnsproviders + verbs: + - get + - list + - watch +- apiGroups: + - dns.cav.enablers.ob + resources: + - pdnsproviders/status + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 4867056..2ce298b 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -4,11 +4,20 @@ kind: ClusterRole metadata: name: manager-role rules: +- apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - watch - apiGroups: - dns.cav.enablers.ob resources: - clusterrrsets - clusterzones + - pdnsproviders - rrsets - zones verbs: @@ -24,6 +33,7 @@ rules: resources: - clusterrrsets/finalizers - clusterzones/finalizers + - pdnsproviders/finalizers - rrsets/finalizers - zones/finalizers verbs: @@ -33,6 +43,7 @@ rules: resources: - clusterrrsets/status - clusterzones/status + - pdnsproviders/status - rrsets/status - zones/status verbs: diff --git a/config/rbac/rrset_admin_role.yaml b/config/rbac/rrset_admin_role.yaml new file mode 100644 index 0000000..5f30c27 --- /dev/null +++ b/config/rbac/rrset_admin_role.yaml @@ -0,0 +1,27 @@ +# This rule is not used by the project powerdns-operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants full permissions ('*') over dns.cav.enablers.ob. +# This role is intended for users authorized to modify roles and bindings within the cluster, +# enabling them to delegate specific permissions to other users or groups as needed. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: powerdns-operator + app.kubernetes.io/managed-by: kustomize + name: rrset-admin-role +rules: +- apiGroups: + - dns.cav.enablers.ob + resources: + - rrsets + verbs: + - '*' +- apiGroups: + - dns.cav.enablers.ob + resources: + - rrsets/status + verbs: + - get diff --git a/config/rbac/zone_admin_role.yaml b/config/rbac/zone_admin_role.yaml new file mode 100644 index 0000000..2f7f052 --- /dev/null +++ b/config/rbac/zone_admin_role.yaml @@ -0,0 +1,27 @@ +# This rule is not used by the project powerdns-operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants full permissions ('*') over dns.cav.enablers.ob. +# This role is intended for users authorized to modify roles and bindings within the cluster, +# enabling them to delegate specific permissions to other users or groups as needed. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: powerdns-operator + app.kubernetes.io/managed-by: kustomize + name: zone-admin-role +rules: +- apiGroups: + - dns.cav.enablers.ob + resources: + - zones + verbs: + - '*' +- apiGroups: + - dns.cav.enablers.ob + resources: + - zones/status + verbs: + - get diff --git a/config/samples/dns_v1alpha3_clusterzone.yaml b/config/samples/dns_v1alpha3_clusterzone.yaml new file mode 100644 index 0000000..3334c53 --- /dev/null +++ b/config/samples/dns_v1alpha3_clusterzone.yaml @@ -0,0 +1,25 @@ +--- +# Direct zone +apiVersion: dns.cav.enablers.ob/v1alpha2 +kind: ClusterZone +metadata: + name: helloworld.com +spec: + providerRef: my-powerdns-cluster + nameservers: + - ns1.helloworld.com + - ns2.helloworld.com + kind: Native + +--- +# Reverse Zone +apiVersion: dns.cav.enablers.ob/v1alpha2 +kind: ClusterZone +metadata: + name: 1.168.192.in-addr.arpa +spec: + providerRef: my-powerdns-cluster + nameservers: + - ns1.helloworld.com + - ns2.helloworld.com + kind: Native diff --git a/config/samples/dns_v1alpha3_pdnsprovider.yaml b/config/samples/dns_v1alpha3_pdnsprovider.yaml new file mode 100644 index 0000000..e4b0d7f --- /dev/null +++ b/config/samples/dns_v1alpha3_pdnsprovider.yaml @@ -0,0 +1,9 @@ +apiVersion: dns.cav.enablers.ob/v1alpha3 +kind: PDNSProvider +metadata: + labels: + app.kubernetes.io/name: powerdns-operator + app.kubernetes.io/managed-by: kustomize + name: pdnsprovider-sample +spec: + # TODO(user): Add fields here diff --git a/config/samples/dns_v1alpha3_zone.yaml b/config/samples/dns_v1alpha3_zone.yaml new file mode 100644 index 0000000..1fed682 --- /dev/null +++ b/config/samples/dns_v1alpha3_zone.yaml @@ -0,0 +1,64 @@ +--- +# Specific Catalog +apiVersion: dns.cav.enablers.ob/v1alpha2 +kind: Zone +metadata: + name: example1.com + namespace: example1 +spec: + providerRef: my-powerdns-cluster + catalog: catalog.test + nameservers: + - ns1.example1.com + - ns2.example1.com + kind: Master + +--- +# Specific SOA_EDIT_API +apiVersion: dns.cav.enablers.ob/v1alpha2 +kind: Zone +metadata: + name: example2.com + namespace: example2 +spec: + providerRef: my-powerdns-cluster + catalog: catalog.test + nameservers: + - ns1.example2.com + - ns2.example2.com + kind: Master + soa_edit_api: EPOCH + +--- +# Fake Zone +apiVersion: dns.cav.enablers.ob/v1alpha2 +kind: Zone +metadata: + annotations: + fake-zone: "Same name as Zone/example2.com/NS:example2" + name: example2.com + namespace: example3 +spec: + providerRef: my-powerdns-cluster + catalog: catalog.test + nameservers: + - ns3.example2.com + - ns4.example2.com + kind: Master + soa_edit_api: EPOCH + +--- +# Fake Zone +apiVersion: dns.cav.enablers.ob/v1alpha2 +kind: Zone +metadata: + annotations: + fake-zone: "Same name as ClusterZone/helloworld.com" + name: helloworld.com + namespace: example3 +spec: + providerRef: my-powerdns-cluster + nameservers: + - ns3.helloworld.com + - ns4.helloworld.com + kind: Native diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index 046682d..03b8f68 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -4,4 +4,5 @@ resources: - dns_v1alpha2_rrset.yaml - dns_v1alpha2_clusterzone.yaml - dns_v1alpha2_clusterrrset.yaml +- dns_v1alpha3_pdnsprovider.yaml #+kubebuilder:scaffold:manifestskustomizesamples diff --git a/docs/introduction/stability-support.md b/docs/introduction/stability-support.md index b5b4e1a..efda4aa 100644 --- a/docs/introduction/stability-support.md +++ b/docs/introduction/stability-support.md @@ -6,7 +6,7 @@ |-----------|-------------------| | **PowerDNS Authoritative** | 4.7, 4.8, 4.9 | | **Kubernetes** | 1.31, 1.32, 1.33 | -| **Go** (for development) | 1.24+ | +| **Go** (for development) | 1.25+ | ## Breaking Changes diff --git a/internal/controller/clusterrrset_controller.go b/internal/controller/clusterrrset_controller.go index fad1d8c..6d519d6 100644 --- a/internal/controller/clusterrrset_controller.go +++ b/internal/controller/clusterrrset_controller.go @@ -26,14 +26,13 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/metrics" - dnsv1alpha2 "github.com/powerdns-operator/powerdns-operator/api/v1alpha2" + dnsv1alpha3 "github.com/powerdns-operator/powerdns-operator/api/v1alpha3" ) // ClusterRRsetReconciler reconciles a ClusterRRset object type ClusterRRsetReconciler struct { client.Client - Scheme *runtime.Scheme - PDNSClient PdnsClienter + Scheme *runtime.Scheme } func init() { @@ -50,7 +49,7 @@ func (r *ClusterRRsetReconciler) Reconcile(ctx context.Context, req ctrl.Request log.Info("Reconcile ClusterRRset", "ClusterRRset.Name", req.Name) // RRset - rrset := &dnsv1alpha2.ClusterRRset{} + rrset := &dnsv1alpha3.ClusterRRset{} err := r.Get(ctx, req.NamespacedName, rrset) if err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) @@ -91,14 +90,14 @@ func (r *ClusterRRsetReconciler) Reconcile(ctx context.Context, req ctrl.Request } // Zone - var zone dnsv1alpha2.GenericZone + var zone dnsv1alpha3.GenericZone switch rrset.Spec.ZoneRef.Kind { //nolint:goconst case "Zone": - zone = &dnsv1alpha2.Zone{} + zone = &dnsv1alpha3.Zone{} //nolint:goconst case "ClusterZone": - zone = &dnsv1alpha2.ClusterZone{} + zone = &dnsv1alpha3.ClusterZone{} } err = r.Get(ctx, client.ObjectKey{Namespace: rrset.Namespace, Name: rrset.Spec.ZoneRef.Name}, zone) if err != nil { @@ -153,6 +152,20 @@ func (r *ClusterRRsetReconciler) Reconcile(ctx context.Context, req ctrl.Request // If a Zone/ClusterZone exists but is in Failed Status zoneIsInFailedStatus := (zone.GetStatus().SyncStatus != nil && *zone.GetStatus().SyncStatus == FAILED_STATUS) if zoneIsInFailedStatus { + // Check if we should retry based on last failure time of the RRset itself (not the zone) + // This prevents the RRset from being stuck if the zone status is stale or hasn't been updated + rrsetLastTransition := getLastRRsetConditionTransition(rrset) + timeSinceLastFailure := time.Since(rrsetLastTransition) + + // Only skip if the RRset failure is very recent (less than 30 seconds) + // This prevents excessive retries while still allowing recovery when parent zone recovers + if timeSinceLastFailure < 30*time.Second && !isModified { + // Update metrics and requeue + updateRrsetsMetrics(getRRsetName(rrset), rrset) + return ctrl.Result{RequeueAfter: 30 * time.Second}, nil + } + + // If enough time has passed or RRset was modified, update status but continue to try original = rrset.DeepCopy() rrset.Status.SyncStatus = ptr.To(FAILED_STATUS) rrset.Status.ObservedGeneration = &rrset.Generation @@ -181,28 +194,39 @@ func (r *ClusterRRsetReconciler) Reconcile(ctx context.Context, req ctrl.Request return ctrl.Result{}, err } } + return ctrl.Result{}, nil } - return ctrl.Result{}, nil + // IMPORTANT: Continue with full reconciliation attempt despite parent zone being in Failed state + // This allows the RRset to recover if the parent zone has actually recovered but its status + // hasn't been updated yet, or if enough time has passed to warrant a retry attempt. + // We fall through to the normal PowerDNS client creation and reconciliation logic below. + } + + // Get the appropriate PowerDNS client + pdnsClient, err := GetPDNSClient(ctx, r.Client, zone.GetSpec().ProviderRef) + if err != nil { + log.Error(err, "Failed to get PowerDNS client") + return ctrl.Result{}, err } - return rrsetReconcile(ctx, rrset, zone, isModified, isDeleted, lastUpdateTime, r.Scheme, r.Client, r.PDNSClient, log) + return rrsetReconcile(ctx, rrset, zone, isModified, isDeleted, lastUpdateTime, r.Scheme, r.Client, pdnsClient, log) } // SetupWithManager sets up the controller with the Manager. func (r *ClusterRRsetReconciler) SetupWithManager(mgr ctrl.Manager) error { // We use indexer to ensure that only one ClusterRRset/RRset exists for DNS entry - if err := mgr.GetFieldIndexer().IndexField(context.Background(), &dnsv1alpha2.ClusterRRset{}, "ClusterRRset.Entry.Name", func(rawObj client.Object) []string { + if err := mgr.GetFieldIndexer().IndexField(context.Background(), &dnsv1alpha3.ClusterRRset{}, "ClusterRRset.Entry.Name", func(rawObj client.Object) []string { // grab the ClusterRRset object, extract its name... var RRsetName string - if rawObj.(*dnsv1alpha2.ClusterRRset).Status.SyncStatus == nil || *rawObj.(*dnsv1alpha2.ClusterRRset).Status.SyncStatus == SUCCEEDED_STATUS { - RRsetName = getRRsetName(rawObj.(*dnsv1alpha2.ClusterRRset)) + "/" + rawObj.(*dnsv1alpha2.ClusterRRset).Spec.Type + if rawObj.(*dnsv1alpha3.ClusterRRset).Status.SyncStatus == nil || *rawObj.(*dnsv1alpha3.ClusterRRset).Status.SyncStatus == SUCCEEDED_STATUS { + RRsetName = getRRsetName(rawObj.(*dnsv1alpha3.ClusterRRset)) + "/" + rawObj.(*dnsv1alpha3.ClusterRRset).Spec.Type } return []string{RRsetName} }); err != nil { return err } return ctrl.NewControllerManagedBy(mgr). - For(&dnsv1alpha2.ClusterRRset{}). + For(&dnsv1alpha3.ClusterRRset{}). Complete(r) } diff --git a/internal/controller/clusterrrset_controller_test.go b/internal/controller/clusterrrset_controller_test.go index 07c9e92..280b744 100644 --- a/internal/controller/clusterrrset_controller_test.go +++ b/internal/controller/clusterrrset_controller_test.go @@ -24,16 +24,17 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - dnsv1alpha2 "github.com/powerdns-operator/powerdns-operator/api/v1alpha2" + dnsv1alpha3 "github.com/powerdns-operator/powerdns-operator/api/v1alpha3" ) var _ = Describe("ClusterRRset Controller", func() { const ( // Zone - zoneName = "example6.org" - zoneKind = NATIVE_KIND_ZONE - zoneNS1 = "ns1.example6.org" - zoneNS2 = "ns2.example6.org" + zoneName = "example6.org" + zoneKind = NATIVE_KIND_ZONE + zoneProviderRef = "test-powerdns" + zoneNS1 = "ns1.example6.org" + zoneNS2 = "ns2.example6.org" // RRset resourceName = "test.example6.org" @@ -67,14 +68,15 @@ var _ = Describe("ClusterRRset Controller", func() { BeforeEach(func() { ctx := context.Background() By("Creating the Zone resource") - zone := &dnsv1alpha2.ClusterZone{ + zone := &dnsv1alpha3.ClusterZone{ ObjectMeta: metav1.ObjectMeta{ Name: zoneName, }, } zone.SetResourceVersion("") _, err := controllerutil.CreateOrUpdate(ctx, k8sClient, zone, func() error { - zone.Spec = dnsv1alpha2.ZoneSpec{ + zone.Spec = dnsv1alpha3.ZoneSpec{ + ProviderRef: zoneProviderRef, Kind: zoneKind, Nameservers: []string{zoneNS1, zoneNS2}, } @@ -92,12 +94,12 @@ var _ = Describe("ClusterRRset Controller", func() { }, timeout, interval).Should(BeTrue()) By("Ensuring the resource does not already exists") - emptyResource := &dnsv1alpha2.ClusterRRset{} + emptyResource := &dnsv1alpha3.ClusterRRset{} err = k8sClient.Get(ctx, clusterRrsetLookupKey, emptyResource) Expect(err).To(HaveOccurred()) By("Creating the ClusterRRset resource") - resource := &dnsv1alpha2.ClusterRRset{ + resource := &dnsv1alpha3.ClusterRRset{ ObjectMeta: metav1.ObjectMeta{ Name: resourceName, }, @@ -105,8 +107,8 @@ var _ = Describe("ClusterRRset Controller", func() { resource.SetResourceVersion("") comment := resourceComment _, err = controllerutil.CreateOrUpdate(ctx, k8sClient, resource, func() error { - resource.Spec = dnsv1alpha2.RRsetSpec{ - ZoneRef: dnsv1alpha2.ZoneRef{ + resource.Spec = dnsv1alpha3.RRsetSpec{ + ZoneRef: dnsv1alpha3.ZoneRef{ Name: zoneRef, Kind: resourceZoneKind, }, @@ -136,7 +138,7 @@ var _ = Describe("ClusterRRset Controller", func() { AfterEach(func() { ctx := context.Background() - resource := &dnsv1alpha2.ClusterRRset{} + resource := &dnsv1alpha3.ClusterRRset{} err := k8sClient.Get(ctx, clusterRrsetLookupKey, resource) Expect(err).NotTo(HaveOccurred()) @@ -150,7 +152,7 @@ var _ = Describe("ClusterRRset Controller", func() { }, timeout, interval).Should(BeTrue()) By("Cleaning up the specific resource instance Zone") - zone := &dnsv1alpha2.ClusterZone{} + zone := &dnsv1alpha3.ClusterZone{} err = k8sClient.Get(ctx, clusterZoneLookupKey, zone) Expect(err).NotTo(HaveOccurred()) Expect(k8sClient.Delete(ctx, zone)).To(Succeed()) @@ -172,7 +174,7 @@ var _ = Describe("ClusterRRset Controller", func() { ic := countClusterRrsetsMetrics() ctx := context.Background() By("Getting the existing resource") - createdResource := &dnsv1alpha2.ClusterRRset{} + createdResource := &dnsv1alpha3.ClusterRRset{} Eventually(func() bool { err := k8sClient.Get(ctx, clusterRrsetLookupKey, createdResource) return err == nil && createdResource.IsInExpectedStatus(FIRST_GENERATION, SUCCEEDED_STATUS) @@ -204,7 +206,7 @@ var _ = Describe("ClusterRRset Controller", func() { recreationResourceZoneName := zoneName By("Creating a RRset") - resource := &dnsv1alpha2.RRset{ + resource := &dnsv1alpha3.RRset{ ObjectMeta: metav1.ObjectMeta{ Name: recreationResourceName, Namespace: recreationResourceNamespace, @@ -212,13 +214,13 @@ var _ = Describe("ClusterRRset Controller", func() { } resource.SetResourceVersion("") _, err := controllerutil.CreateOrUpdate(ctx, k8sClient, resource, func() error { - resource.Spec = dnsv1alpha2.RRsetSpec{ + resource.Spec = dnsv1alpha3.RRsetSpec{ Type: recreationResourceType, Name: recreationResourceDNSName, TTL: recreationResourceTTL, Records: recreationResourceRecords, Comment: &recreationResourceComment, - ZoneRef: dnsv1alpha2.ZoneRef{ + ZoneRef: dnsv1alpha3.ZoneRef{ Name: recreationResourceZoneName, Kind: recreationResourceZoneKind, }, @@ -228,7 +230,7 @@ var _ = Describe("ClusterRRset Controller", func() { Expect(err).NotTo(HaveOccurred()) By("Getting the resource") - recreatedRrset := &dnsv1alpha2.RRset{} + recreatedRrset := &dnsv1alpha3.RRset{} typeNamespacedName := types.NamespacedName{ Name: recreationResourceName, Namespace: recreationResourceNamespace, diff --git a/internal/controller/clusterzone_controller.go b/internal/controller/clusterzone_controller.go index 7617438..bd60e90 100644 --- a/internal/controller/clusterzone_controller.go +++ b/internal/controller/clusterzone_controller.go @@ -22,14 +22,13 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/metrics" - dnsv1alpha2 "github.com/powerdns-operator/powerdns-operator/api/v1alpha2" + dnsv1alpha3 "github.com/powerdns-operator/powerdns-operator/api/v1alpha3" ) // ClusterZoneReconciler reconciles a ClusterZone object type ClusterZoneReconciler struct { client.Client - Scheme *runtime.Scheme - PDNSClient PdnsClienter + Scheme *runtime.Scheme } func init() { @@ -46,7 +45,7 @@ func (r *ClusterZoneReconciler) Reconcile(ctx context.Context, req ctrl.Request) log.Info("Reconcile ClusterZone", "ClusterZone.Name", req.Name) // Get ClusterZone - zone := &dnsv1alpha2.ClusterZone{} + zone := &dnsv1alpha3.ClusterZone{} err := r.Get(ctx, req.NamespacedName, zone) if err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) @@ -81,25 +80,32 @@ func (r *ClusterZoneReconciler) Reconcile(ctx context.Context, req ctrl.Request) } } - return zoneReconcile(ctx, zone, isModified, isDeleted, r.Client, r.PDNSClient, log) + // Get the appropriate PowerDNS client + pdnsClient, err := GetPDNSClient(ctx, r.Client, zone.GetProviderRef()) + if err != nil { + log.Error(err, "Failed to get PowerDNS client") + return ctrl.Result{}, err + } + + return zoneReconcile(ctx, zone, isModified, isDeleted, r.Client, pdnsClient, log) } // SetupWithManager sets up the controller with the Manager. func (r *ClusterZoneReconciler) SetupWithManager(mgr ctrl.Manager) error { // We use indexer to ensure that only one Zone/ClusterZone exists for one DNS entry - if err := mgr.GetFieldIndexer().IndexField(context.Background(), &dnsv1alpha2.ClusterZone{}, "ClusterZone.Entry.Name", func(rawObj client.Object) []string { + if err := mgr.GetFieldIndexer().IndexField(context.Background(), &dnsv1alpha3.ClusterZone{}, "ClusterZone.Entry.Name", func(rawObj client.Object) []string { // grab the ClusterZone object, extract its name... var ZoneName string - if rawObj.(*dnsv1alpha2.ClusterZone).Status.SyncStatus == nil || *rawObj.(*dnsv1alpha2.ClusterZone).Status.SyncStatus == SUCCEEDED_STATUS { - ZoneName = (rawObj.(*dnsv1alpha2.ClusterZone)).Name + if rawObj.(*dnsv1alpha3.ClusterZone).Status.SyncStatus == nil || *rawObj.(*dnsv1alpha3.ClusterZone).Status.SyncStatus == SUCCEEDED_STATUS { + ZoneName = (rawObj.(*dnsv1alpha3.ClusterZone)).Name } return []string{ZoneName} }); err != nil { return err } return ctrl.NewControllerManagedBy(mgr). - For(&dnsv1alpha2.ClusterZone{}). - Owns(&dnsv1alpha2.ClusterRRset{}). - Owns(&dnsv1alpha2.RRset{}). + For(&dnsv1alpha3.ClusterZone{}). + Owns(&dnsv1alpha3.ClusterRRset{}). + Owns(&dnsv1alpha3.RRset{}). Complete(r) } diff --git a/internal/controller/clusterzone_controller_test.go b/internal/controller/clusterzone_controller_test.go index 53ba048..1a7c545 100644 --- a/internal/controller/clusterzone_controller_test.go +++ b/internal/controller/clusterzone_controller_test.go @@ -26,14 +26,15 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - dnsv1alpha2 "github.com/powerdns-operator/powerdns-operator/api/v1alpha2" + dnsv1alpha3 "github.com/powerdns-operator/powerdns-operator/api/v1alpha3" ) var _ = Describe("ClusterZone Controller", func() { const ( - resourceName = "example4.org" - resourceKind = NATIVE_KIND_ZONE - resourceCatalog = "catalog.example4.org." + resourceName = "example4.org" + resourceKind = NATIVE_KIND_ZONE + resourceCatalog = "catalog.example4.org." + resourceProviderRef = "test-powerdns" timeout = time.Second * 5 interval = time.Millisecond * 250 @@ -48,14 +49,15 @@ var _ = Describe("ClusterZone Controller", func() { ctx := context.Background() By("creating the ClusterZone resource") - resource := &dnsv1alpha2.ClusterZone{ + resource := &dnsv1alpha3.ClusterZone{ ObjectMeta: metav1.ObjectMeta{ Name: resourceName, }, } resource.SetResourceVersion("") _, err := controllerutil.CreateOrUpdate(ctx, k8sClient, resource, func() error { - resource.Spec = dnsv1alpha2.ZoneSpec{ + resource.Spec = dnsv1alpha3.ZoneSpec{ + ProviderRef: resourceProviderRef, Kind: resourceKind, Nameservers: resourceNameservers, Catalog: ptr.To(resourceCatalog), @@ -79,7 +81,7 @@ var _ = Describe("ClusterZone Controller", func() { AfterEach(func() { ctx := context.Background() - resource := &dnsv1alpha2.ClusterZone{} + resource := &dnsv1alpha3.ClusterZone{} err := k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -104,7 +106,7 @@ var _ = Describe("ClusterZone Controller", func() { ic := countClusterZonesMetrics() ctx := context.Background() By("Getting the existing resource") - clusterzone := &dnsv1alpha2.ClusterZone{} + clusterzone := &dnsv1alpha3.ClusterZone{} Eventually(func() bool { err := k8sClient.Get(ctx, typeNamespacedName, clusterzone) return err == nil && clusterzone.IsInExpectedStatus(FIRST_GENERATION, SUCCEEDED_STATUS) @@ -129,7 +131,7 @@ var _ = Describe("ClusterZone Controller", func() { recreationResourceNameservers := []string{"ns1.example4.org", "ns2.example4.org"} By("Creating a Zone") - resource := &dnsv1alpha2.Zone{ + resource := &dnsv1alpha3.Zone{ ObjectMeta: metav1.ObjectMeta{ Name: recreationResourceName, Namespace: recreationResourceNamespace, @@ -137,7 +139,8 @@ var _ = Describe("ClusterZone Controller", func() { } resource.SetResourceVersion("") _, err := controllerutil.CreateOrUpdate(ctx, k8sClient, resource, func() error { - resource.Spec = dnsv1alpha2.ZoneSpec{ + resource.Spec = dnsv1alpha3.ZoneSpec{ + ProviderRef: resourceProviderRef, Kind: recreationResourceKind, Nameservers: recreationResourceNameservers, Catalog: ptr.To(recreationResourceCatalog), @@ -147,7 +150,7 @@ var _ = Describe("ClusterZone Controller", func() { Expect(err).NotTo(HaveOccurred()) By("Getting the resource") - updatedZone := &dnsv1alpha2.Zone{} + updatedZone := &dnsv1alpha3.Zone{} typeNamespacedName := types.NamespacedName{ Name: recreationResourceName, Namespace: recreationResourceNamespace, diff --git a/internal/controller/common.go b/internal/controller/common.go index d911e5e..c1c1bdb 100644 --- a/internal/controller/common.go +++ b/internal/controller/common.go @@ -13,12 +13,13 @@ package controller import ( "context" + "fmt" "strings" "time" "github.com/go-logr/logr" "github.com/joeig/go-powerdns/v3" - dnsv1alpha2 "github.com/powerdns-operator/powerdns-operator/api/v1alpha2" + dnsv1alpha3 "github.com/powerdns-operator/powerdns-operator/api/v1alpha3" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -29,7 +30,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) -func zoneReconcile(ctx context.Context, gz dnsv1alpha2.GenericZone, isModified bool, isDeleted bool, cl client.Client, PDNSClient PdnsClienter, log logr.Logger) (ctrl.Result, error) { +func zoneReconcile(ctx context.Context, gz dnsv1alpha3.GenericZone, isModified bool, isDeleted bool, cl client.Client, PDNSClient PdnsClienter, log logr.Logger) (ctrl.Result, error) { isInFailedStatus := (gz.GetStatus().SyncStatus != nil && *gz.GetStatus().SyncStatus == FAILED_STATUS) // examine DeletionTimestamp to determine if object is under deletion @@ -74,21 +75,31 @@ func zoneReconcile(ctx context.Context, gz dnsv1alpha2.GenericZone, isModified b } // We cannot exit previously (at the early moments of reconcile), because we have to allow deletion process + // For failed resources, only skip reconciliation if it was very recent to avoid excessive retries if isInFailedStatus && !isModified { - // Update resource metrics - updateZonesMetrics(gz) - return ctrl.Result{}, nil + // Check if we should retry based on last failure time + lastTransition := getLastConditionTransition(gz) + timeSinceLastFailure := time.Since(lastTransition) + + // Only skip if the failure is very recent (less than 30 seconds) + // This prevents excessive retries while still allowing recovery + if timeSinceLastFailure < 30*time.Second { + // Update resource metrics + updateZonesMetrics(gz) + return ctrl.Result{RequeueAfter: 30 * time.Second}, nil + } + // If enough time has passed, continue with reconciliation to retry } // If a Zone already exists with the same DNS name: // * Stop reconciliation // * Append a Failed Status on Zone - var existingZones dnsv1alpha2.ZoneList + var existingZones dnsv1alpha3.ZoneList if err := cl.List(ctx, &existingZones, client.MatchingFields{"Zone.Entry.Name": gz.GetName()}); err != nil { log.Error(err, "unable to find Zone related to the DNS Name") return ctrl.Result{}, err } - var existingClusterZones dnsv1alpha2.ClusterZoneList + var existingClusterZones dnsv1alpha3.ClusterZoneList if err := cl.List(ctx, &existingClusterZones, client.MatchingFields{"ClusterZone.Entry.Name": gz.GetName()}); err != nil { log.Error(err, "unable to find ClusterZone related to the DNS Name") return ctrl.Result{}, err @@ -109,7 +120,7 @@ func zoneReconcile(ctx context.Context, gz dnsv1alpha2.GenericZone, isModified b Reason: ZoneReasonDuplicated, Message: ZoneMessageDuplicated, }) - gz.SetStatus(dnsv1alpha2.ZoneStatus{ + gz.SetStatus(dnsv1alpha3.ZoneStatus{ SyncStatus: ptr.To(FAILED_STATUS), ObservedGeneration: &gz.GetObjectMeta().Generation, Conditions: conditions, @@ -125,27 +136,38 @@ func zoneReconcile(ctx context.Context, gz dnsv1alpha2.GenericZone, isModified b return ctrl.Result{}, nil } - // Get zone + // Attempt to retrieve zone information from PowerDNS API + // This is where we actually test connectivity and sync with the PowerDNS backend. + // Connection failures are handled gracefully by updating the resource status rather than failing hard. zoneRes, err := getZoneExternalResources(ctx, gz.GetObjectMeta().Name, PDNSClient, log) - if err != nil { - return ctrl.Result{}, err - } + var syncStatus *string + var conditionMessage, conditionReason string + var conditionStatus metav1.ConditionStatus - syncStatus, conditionMessage, conditionReason, conditionStatus, err := zoneExternalResourcesReconcile(ctx, zoneRes, gz, PDNSClient, log) if err != nil { - return ctrl.Result{}, err + // Connection failed - update status to reflect the failure + log.Error(err, "Failed to connect to PowerDNS API") + syncStatus = ptr.To(FAILED_STATUS) + conditionStatus = metav1.ConditionFalse + conditionReason = ZoneReasonSynchronizationFailed + conditionMessage = fmt.Sprintf("Failed to connect to PowerDNS API: %v", err) + } else { + // Connection successful - proceed with reconciliation + var reconcileErr error + syncStatus, conditionMessage, conditionReason, conditionStatus, reconcileErr = zoneExternalResourcesReconcile(ctx, zoneRes, gz, PDNSClient, log) + if reconcileErr != nil { + log.Error(reconcileErr, "Failed to reconcile zone external resources") + syncStatus = ptr.To(FAILED_STATUS) + conditionStatus = metav1.ConditionFalse + conditionReason = ZoneReasonSynchronizationFailed + conditionMessage = fmt.Sprintf("Reconciliation failed: %v", reconcileErr) + } } if syncStatus == nil { syncStatus = ptr.To(SUCCEEDED_STATUS) } - // Update ZoneStatus - zoneRes, err = getZoneExternalResources(ctx, gz.GetObjectMeta().Name, PDNSClient, log) - if err != nil { - return ctrl.Result{}, err - } - err = patchZoneStatus(ctx, gz, zoneRes, syncStatus, cl, metav1.Condition{ Type: "Available", LastTransitionTime: metav1.NewTime(time.Now().UTC()), @@ -167,7 +189,7 @@ func zoneReconcile(ctx context.Context, gz dnsv1alpha2.GenericZone, isModified b return ctrl.Result{}, nil } -func rrsetReconcile(ctx context.Context, gr dnsv1alpha2.GenericRRset, zone dnsv1alpha2.GenericZone, isModified bool, isDeleted bool, lastUpdateTime *metav1.Time, scheme *runtime.Scheme, cl client.Client, PDNSClient PdnsClienter, log logr.Logger) (ctrl.Result, error) { +func rrsetReconcile(ctx context.Context, gr dnsv1alpha3.GenericRRset, zone dnsv1alpha3.GenericZone, isModified bool, isDeleted bool, lastUpdateTime *metav1.Time, scheme *runtime.Scheme, cl client.Client, PDNSClient PdnsClienter, log logr.Logger) (ctrl.Result, error) { isInFailedStatus := (gr.GetStatus().SyncStatus != nil && *gr.GetStatus().SyncStatus == FAILED_STATUS) // initialize syncStatus @@ -233,12 +255,12 @@ func rrsetReconcile(ctx context.Context, gr dnsv1alpha2.GenericRRset, zone dnsv1 // If a RRset already exists with the same DNS name: // * Stop reconciliation // * Append a Failed Status on RRset - var existingRRsets dnsv1alpha2.RRsetList + var existingRRsets dnsv1alpha3.RRsetList if err := cl.List(ctx, &existingRRsets, client.MatchingFields{"RRset.Entry.Name": getRRsetName(gr) + "/" + gr.GetSpec().Type}); err != nil { log.Error(err, "unable to find RRsets related to the DNS Name") return ctrl.Result{}, err } - var existingClusterRRsets dnsv1alpha2.ClusterRRsetList + var existingClusterRRsets dnsv1alpha3.ClusterRRsetList if err := cl.List(ctx, &existingClusterRRsets, client.MatchingFields{"ClusterRRset.Entry.Name": getRRsetName(gr) + "/" + gr.GetSpec().Type}); err != nil { log.Error(err, "unable to find RRsets related to the DNS Name") return ctrl.Result{}, err @@ -260,7 +282,7 @@ func rrsetReconcile(ctx context.Context, gr dnsv1alpha2.GenericRRset, zone dnsv1 Message: RrsetMessageDuplicated, }) name := getRRsetName(gr) - gr.SetStatus(dnsv1alpha2.RRsetStatus{ + gr.SetStatus(dnsv1alpha3.RRsetStatus{ LastUpdateTime: lastUpdateTime, DnsEntryName: &name, SyncStatus: ptr.To(FAILED_STATUS), @@ -319,7 +341,7 @@ func rrsetReconcile(ctx context.Context, gr dnsv1alpha2.GenericRRset, zone dnsv1 Message: conditionMessage, }) name := getRRsetName(gr) - gr.SetStatus(dnsv1alpha2.RRsetStatus{ + gr.SetStatus(dnsv1alpha3.RRsetStatus{ LastUpdateTime: lastUpdateTime, DnsEntryName: &name, SyncStatus: syncStatus, @@ -347,7 +369,7 @@ func getZoneExternalResources(ctx context.Context, domain string, PDNSClient Pdn return zoneRes, nil } -func createZoneExternalResources(ctx context.Context, zone dnsv1alpha2.GenericZone, PDNSClient PdnsClienter, log logr.Logger) error { +func createZoneExternalResources(ctx context.Context, zone dnsv1alpha3.GenericZone, PDNSClient PdnsClienter, log logr.Logger) error { // Make Nameservers canonical for i, ns := range zone.GetSpec().Nameservers { zone.GetSpec().Nameservers[i] = makeCanonical(ns) @@ -378,7 +400,7 @@ func createZoneExternalResources(ctx context.Context, zone dnsv1alpha2.GenericZo return nil } -func updateZoneExternalResources(ctx context.Context, zone dnsv1alpha2.GenericZone, PDNSClient PdnsClienter, log logr.Logger) error { +func updateZoneExternalResources(ctx context.Context, zone dnsv1alpha3.GenericZone, PDNSClient PdnsClienter, log logr.Logger) error { zoneKind := powerdns.ZoneKind(zone.GetSpec().Kind) // Make Catalog canonical @@ -401,7 +423,7 @@ func updateZoneExternalResources(ctx context.Context, zone dnsv1alpha2.GenericZo return nil } -func updateNsOnZoneExternalResources(ctx context.Context, zone dnsv1alpha2.GenericZone, ttl uint32, PDNSClient PdnsClienter, log logr.Logger) error { +func updateNsOnZoneExternalResources(ctx context.Context, zone dnsv1alpha3.GenericZone, ttl uint32, PDNSClient PdnsClienter, log logr.Logger) error { nameserversCanonical := []string{} for _, n := range zone.GetSpec().Nameservers { nameserversCanonical = append(nameserversCanonical, makeCanonical(n)) @@ -415,7 +437,7 @@ func updateNsOnZoneExternalResources(ctx context.Context, zone dnsv1alpha2.Gener return nil } -func deleteZoneExternalResources(ctx context.Context, zone dnsv1alpha2.GenericZone, PDNSClient PdnsClienter, log logr.Logger) error { +func deleteZoneExternalResources(ctx context.Context, zone dnsv1alpha3.GenericZone, PDNSClient PdnsClienter, log logr.Logger) error { err := PDNSClient.Zones.Delete(ctx, zone.GetObjectMeta().Name) // Zone may have already been deleted and it is not an error if err != nil && err.Error() != ZONE_NOT_FOUND_MSG { @@ -425,7 +447,7 @@ func deleteZoneExternalResources(ctx context.Context, zone dnsv1alpha2.GenericZo return nil } -func zoneExternalResourcesReconcile(ctx context.Context, zoneRes *powerdns.Zone, gz dnsv1alpha2.GenericZone, PDNSClient PdnsClienter, log logr.Logger) (*string, string, string, metav1.ConditionStatus, error) { +func zoneExternalResourcesReconcile(ctx context.Context, zoneRes *powerdns.Zone, gz dnsv1alpha3.GenericZone, PDNSClient PdnsClienter, log logr.Logger) (*string, string, string, metav1.ConditionStatus, error) { // Initialization var syncStatus *string conditionStatus := metav1.ConditionTrue @@ -496,30 +518,38 @@ func zoneExternalResourcesReconcile(ctx context.Context, zoneRes *powerdns.Zone, return syncStatus, conditionMessage, conditionReason, conditionStatus, nil } -func patchZoneStatus(ctx context.Context, zone dnsv1alpha2.GenericZone, zoneRes *powerdns.Zone, status *string, cl client.Client, condition metav1.Condition) error { +func patchZoneStatus(ctx context.Context, zone dnsv1alpha3.GenericZone, zoneRes *powerdns.Zone, status *string, cl client.Client, condition metav1.Condition) error { original := zone.Copy() - kind := string(ptr.Deref(zoneRes.Kind, "")) conditions := zone.GetStatus().Conditions meta.SetStatusCondition(&conditions, condition) - zone.SetStatus(dnsv1alpha2.ZoneStatus{ - ID: zoneRes.ID, - Name: zoneRes.Name, - Kind: &kind, - Serial: zoneRes.Serial, - NotifiedSerial: zoneRes.NotifiedSerial, - EditedSerial: zoneRes.EditedSerial, - Masters: zoneRes.Masters, - DNSsec: zoneRes.DNSsec, + + // Create base status with minimal required fields + zoneStatus := dnsv1alpha3.ZoneStatus{ SyncStatus: status, - Catalog: zoneRes.Catalog, ObservedGeneration: ptr.To(zone.GetGeneration()), Conditions: conditions, - }) + } + + // If we have zone data from PowerDNS, include it in the status + if zoneRes != nil { + kind := string(ptr.Deref(zoneRes.Kind, "")) + zoneStatus.ID = zoneRes.ID + zoneStatus.Name = zoneRes.Name + zoneStatus.Kind = &kind + zoneStatus.Serial = zoneRes.Serial + zoneStatus.NotifiedSerial = zoneRes.NotifiedSerial + zoneStatus.EditedSerial = zoneRes.EditedSerial + zoneStatus.Masters = zoneRes.Masters + zoneStatus.DNSsec = zoneRes.DNSsec + zoneStatus.Catalog = zoneRes.Catalog + } + + zone.SetStatus(zoneStatus) return cl.Status().Patch(ctx, zone, client.MergeFrom(original)) } -func deleteRrsetExternalResources(ctx context.Context, zone dnsv1alpha2.GenericZone, rrset dnsv1alpha2.GenericRRset, PDNSClient PdnsClienter, log logr.Logger) error { +func deleteRrsetExternalResources(ctx context.Context, zone dnsv1alpha3.GenericZone, rrset dnsv1alpha3.GenericRRset, PDNSClient PdnsClienter, log logr.Logger) error { err := PDNSClient.Records.Delete(ctx, zone.GetObjectMeta().Name, getRRsetName(rrset), powerdns.RRType(rrset.GetSpec().Type)) if err != nil { log.Error(err, "Failed to delete record") @@ -529,7 +559,7 @@ func deleteRrsetExternalResources(ctx context.Context, zone dnsv1alpha2.GenericZ return nil } -func createOrUpdateRrsetExternalResources(ctx context.Context, zone dnsv1alpha2.GenericZone, rrset dnsv1alpha2.GenericRRset, PDNSClient PdnsClienter) (bool, error) { +func createOrUpdateRrsetExternalResources(ctx context.Context, zone dnsv1alpha3.GenericZone, rrset dnsv1alpha3.GenericRRset, PDNSClient PdnsClienter) (bool, error) { name := getRRsetName(rrset) rrType := powerdns.RRType(rrset.GetSpec().Type) // Looking for a record with same Name and Type @@ -565,7 +595,7 @@ func createOrUpdateRrsetExternalResources(ctx context.Context, zone dnsv1alpha2. return true, nil } -func ownObject(ctx context.Context, zone dnsv1alpha2.GenericZone, rrset dnsv1alpha2.GenericRRset, scheme *runtime.Scheme, cl client.Client, log logr.Logger) error { +func ownObject(ctx context.Context, zone dnsv1alpha3.GenericZone, rrset dnsv1alpha3.GenericRRset, scheme *runtime.Scheme, cl client.Client, log logr.Logger) error { err := ctrl.SetControllerReference(zone, rrset, scheme) if err != nil { log.Error(err, "Failed to set owner reference. Is there already a controller managing this object?") @@ -573,3 +603,51 @@ func ownObject(ctx context.Context, zone dnsv1alpha2.GenericZone, rrset dnsv1alp } return cl.Update(ctx, rrset) } + +// getLastConditionTransition returns the time when a Zone/ClusterZone last changed its condition status. +func getLastConditionTransition(gz dnsv1alpha3.GenericZone) time.Time { + conditions := gz.GetStatus().Conditions + if len(conditions) == 0 { + // New resource with no status conditions yet - return old time to allow immediate retry + return time.Now().Add(-time.Hour) + } + + // Find the most recent condition transition across all condition types + var latest time.Time + for _, condition := range conditions { + if condition.LastTransitionTime.After(latest) { + latest = condition.LastTransitionTime.Time + } + } + + if latest.IsZero() { + // Safety fallback: if no valid timestamps found, return old time to allow retry + return time.Now().Add(-time.Hour) + } + + return latest +} + +// getLastRRsetConditionTransition returns the time when an RRset/ClusterRRset last changed its condition status. +func getLastRRsetConditionTransition(gr dnsv1alpha3.GenericRRset) time.Time { + conditions := gr.GetStatus().Conditions + if len(conditions) == 0 { + // New resource with no status conditions yet - return old time to allow immediate retry + return time.Now().Add(-time.Hour) + } + + // Find the most recent condition transition across all condition types + var latest time.Time + for _, condition := range conditions { + if condition.LastTransitionTime.After(latest) { + latest = condition.LastTransitionTime.Time + } + } + + if latest.IsZero() { + // Safety fallback: if no valid timestamps found, return old time to allow retry + return time.Now().Add(-time.Hour) + } + + return latest +} diff --git a/internal/controller/common_test.go b/internal/controller/common_test.go index 6059df6..7b4d2ee 100644 --- a/internal/controller/common_test.go +++ b/internal/controller/common_test.go @@ -21,7 +21,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/joeig/go-powerdns/v3" - dnsv1alpha2 "github.com/powerdns-operator/powerdns-operator/api/v1alpha2" + dnsv1alpha3 "github.com/powerdns-operator/powerdns-operator/api/v1alpha3" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/log" ) @@ -138,13 +138,13 @@ func TestCreateExternalResources(t *testing.T) { var testCases = []struct { description string - genericZone dnsv1alpha2.GenericZone + genericZone dnsv1alpha3.GenericZone e error }{ - {"Valid Zone", &dnsv1alpha2.Zone{ObjectMeta: metav1.ObjectMeta{Name: name1, Namespace: namespace1}, Spec: dnsv1alpha2.ZoneSpec{Kind: MASTER_KIND_ZONE, Nameservers: nameservers1, Catalog: &catalog, SOAEditAPI: &soaEditApi1}}, nil}, - {"Valid ClusterZone", &dnsv1alpha2.ClusterZone{ObjectMeta: metav1.ObjectMeta{Name: name2}, Spec: dnsv1alpha2.ZoneSpec{Kind: MASTER_KIND_ZONE, Nameservers: nameservers2, Catalog: &catalog, SOAEditAPI: &soaEditApi2}}, nil}, - {"Already existing Zone", &dnsv1alpha2.Zone{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace}, Spec: dnsv1alpha2.ZoneSpec{Kind: MASTER_KIND_ZONE, Nameservers: nameservers, Catalog: &catalog, SOAEditAPI: &soaEditApi}}, powerdns.Error{StatusCode: 409, Status: "409 Conflict", Message: "Conflict"}}, - {"communication error", &dnsv1alpha2.Zone{ObjectMeta: metav1.ObjectMeta{Name: FAKE_SITE, Namespace: namespace}, Spec: dnsv1alpha2.ZoneSpec{Kind: MASTER_KIND_ZONE, Nameservers: nameservers, Catalog: &catalog, SOAEditAPI: &soaEditApi}}, &powerdns.Error{StatusCode: 500, Status: "500 Internal Server Error", Message: "Internal Server Error"}}, + {"Valid Zone", &dnsv1alpha3.Zone{ObjectMeta: metav1.ObjectMeta{Name: name1, Namespace: namespace1}, Spec: dnsv1alpha3.ZoneSpec{Kind: MASTER_KIND_ZONE, Nameservers: nameservers1, Catalog: &catalog, SOAEditAPI: &soaEditApi1}}, nil}, + {"Valid ClusterZone", &dnsv1alpha3.ClusterZone{ObjectMeta: metav1.ObjectMeta{Name: name2}, Spec: dnsv1alpha3.ZoneSpec{Kind: MASTER_KIND_ZONE, Nameservers: nameservers2, Catalog: &catalog, SOAEditAPI: &soaEditApi2}}, nil}, + {"Already existing Zone", &dnsv1alpha3.Zone{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace}, Spec: dnsv1alpha3.ZoneSpec{Kind: MASTER_KIND_ZONE, Nameservers: nameservers, Catalog: &catalog, SOAEditAPI: &soaEditApi}}, powerdns.Error{StatusCode: 409, Status: "409 Conflict", Message: "Conflict"}}, + {"communication error", &dnsv1alpha3.Zone{ObjectMeta: metav1.ObjectMeta{Name: FAKE_SITE, Namespace: namespace}, Spec: dnsv1alpha3.ZoneSpec{Kind: MASTER_KIND_ZONE, Nameservers: nameservers, Catalog: &catalog, SOAEditAPI: &soaEditApi}}, &powerdns.Error{StatusCode: 500, Status: "500 Internal Server Error", Message: "Internal Server Error"}}, } // Mock initialization @@ -183,14 +183,14 @@ func TestUpdateExternalResources(t *testing.T) { var testCases = []struct { description string - genericZone dnsv1alpha2.GenericZone + genericZone dnsv1alpha3.GenericZone e error }{ - {"Valid Zone", &dnsv1alpha2.Zone{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace}, Spec: dnsv1alpha2.ZoneSpec{Kind: SLAVE_KIND_ZONE, Nameservers: nameservers, Catalog: &catalog, SOAEditAPI: &soaEditApi}}, nil}, - {"Valid ClusterZone", &dnsv1alpha2.ClusterZone{ObjectMeta: metav1.ObjectMeta{Name: name}, Spec: dnsv1alpha2.ZoneSpec{Kind: NATIVE_KIND_ZONE, Nameservers: nameservers, Catalog: &catalog, SOAEditAPI: &soaEditApi}}, nil}, - {"Non-existing Zone", &dnsv1alpha2.Zone{ObjectMeta: metav1.ObjectMeta{Name: name1, Namespace: namespace1}, Spec: dnsv1alpha2.ZoneSpec{Kind: MASTER_KIND_ZONE, Nameservers: nameservers1, Catalog: &catalog, SOAEditAPI: &soaEditApi1}}, powerdns.Error{StatusCode: 404, Status: "404 Not Found", Message: "Not Found"}}, - {"Non-existing ClusterZone", &dnsv1alpha2.ClusterZone{ObjectMeta: metav1.ObjectMeta{Name: name2}, Spec: dnsv1alpha2.ZoneSpec{Kind: MASTER_KIND_ZONE, Nameservers: nameservers2, Catalog: &catalog, SOAEditAPI: &soaEditApi2}}, powerdns.Error{StatusCode: 404, Status: "404 Not Found", Message: "Not Found"}}, - {"communication error", &dnsv1alpha2.Zone{ObjectMeta: metav1.ObjectMeta{Name: FAKE_SITE, Namespace: namespace}, Spec: dnsv1alpha2.ZoneSpec{Kind: MASTER_KIND_ZONE, Nameservers: nameservers, Catalog: &catalog, SOAEditAPI: &soaEditApi}}, &powerdns.Error{StatusCode: 500, Status: "500 Internal Server Error", Message: "Internal Server Error"}}, + {"Valid Zone", &dnsv1alpha3.Zone{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace}, Spec: dnsv1alpha3.ZoneSpec{Kind: SLAVE_KIND_ZONE, Nameservers: nameservers, Catalog: &catalog, SOAEditAPI: &soaEditApi}}, nil}, + {"Valid ClusterZone", &dnsv1alpha3.ClusterZone{ObjectMeta: metav1.ObjectMeta{Name: name}, Spec: dnsv1alpha3.ZoneSpec{Kind: NATIVE_KIND_ZONE, Nameservers: nameservers, Catalog: &catalog, SOAEditAPI: &soaEditApi}}, nil}, + {"Non-existing Zone", &dnsv1alpha3.Zone{ObjectMeta: metav1.ObjectMeta{Name: name1, Namespace: namespace1}, Spec: dnsv1alpha3.ZoneSpec{Kind: MASTER_KIND_ZONE, Nameservers: nameservers1, Catalog: &catalog, SOAEditAPI: &soaEditApi1}}, powerdns.Error{StatusCode: 404, Status: "404 Not Found", Message: "Not Found"}}, + {"Non-existing ClusterZone", &dnsv1alpha3.ClusterZone{ObjectMeta: metav1.ObjectMeta{Name: name2}, Spec: dnsv1alpha3.ZoneSpec{Kind: MASTER_KIND_ZONE, Nameservers: nameservers2, Catalog: &catalog, SOAEditAPI: &soaEditApi2}}, powerdns.Error{StatusCode: 404, Status: "404 Not Found", Message: "Not Found"}}, + {"communication error", &dnsv1alpha3.Zone{ObjectMeta: metav1.ObjectMeta{Name: FAKE_SITE, Namespace: namespace}, Spec: dnsv1alpha3.ZoneSpec{Kind: MASTER_KIND_ZONE, Nameservers: nameservers, Catalog: &catalog, SOAEditAPI: &soaEditApi}}, &powerdns.Error{StatusCode: 500, Status: "500 Internal Server Error", Message: "Internal Server Error"}}, } // Mock initialization @@ -224,12 +224,12 @@ func TestUpdateNsOnExternalResources(t *testing.T) { var testCases = []struct { description string - genericZone dnsv1alpha2.GenericZone + genericZone dnsv1alpha3.GenericZone e error }{ - {"Valid Zone", &dnsv1alpha2.Zone{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace}, Spec: dnsv1alpha2.ZoneSpec{Kind: MASTER_KIND_ZONE, Nameservers: nameservers1, Catalog: &catalog, SOAEditAPI: &soaEditApi}}, nil}, - {"Valid ClusterZone", &dnsv1alpha2.ClusterZone{ObjectMeta: metav1.ObjectMeta{Name: name}, Spec: dnsv1alpha2.ZoneSpec{Kind: NATIVE_KIND_ZONE, Nameservers: nameservers2, Catalog: &catalog, SOAEditAPI: &soaEditApi}}, nil}, - {"communication error", &dnsv1alpha2.Zone{ObjectMeta: metav1.ObjectMeta{Name: FAKE_SITE, Namespace: namespace}, Spec: dnsv1alpha2.ZoneSpec{Kind: MASTER_KIND_ZONE, Nameservers: nameservers, Catalog: &catalog, SOAEditAPI: &soaEditApi}}, &powerdns.Error{StatusCode: 500, Status: "500 Internal Server Error", Message: "Internal Server Error"}}, + {"Valid Zone", &dnsv1alpha3.Zone{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace}, Spec: dnsv1alpha3.ZoneSpec{Kind: MASTER_KIND_ZONE, Nameservers: nameservers1, Catalog: &catalog, SOAEditAPI: &soaEditApi}}, nil}, + {"Valid ClusterZone", &dnsv1alpha3.ClusterZone{ObjectMeta: metav1.ObjectMeta{Name: name}, Spec: dnsv1alpha3.ZoneSpec{Kind: NATIVE_KIND_ZONE, Nameservers: nameservers2, Catalog: &catalog, SOAEditAPI: &soaEditApi}}, nil}, + {"communication error", &dnsv1alpha3.Zone{ObjectMeta: metav1.ObjectMeta{Name: FAKE_SITE, Namespace: namespace}, Spec: dnsv1alpha3.ZoneSpec{Kind: MASTER_KIND_ZONE, Nameservers: nameservers, Catalog: &catalog, SOAEditAPI: &soaEditApi}}, &powerdns.Error{StatusCode: 500, Status: "500 Internal Server Error", Message: "Internal Server Error"}}, } // Mock initialization @@ -267,13 +267,13 @@ func TestDeleteExternalResources(t *testing.T) { var testCases = []struct { description string - genericZone dnsv1alpha2.GenericZone + genericZone dnsv1alpha3.GenericZone e error }{ - {"Valid Zone", &dnsv1alpha2.Zone{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace}, Spec: dnsv1alpha2.ZoneSpec{Kind: MASTER_KIND_ZONE, Nameservers: nameservers1, Catalog: &catalog, SOAEditAPI: &soaEditApi}}, nil}, - {"Non-existing Zone", &dnsv1alpha2.Zone{ObjectMeta: metav1.ObjectMeta{Name: name1, Namespace: namespace1}, Spec: dnsv1alpha2.ZoneSpec{Kind: MASTER_KIND_ZONE, Nameservers: nameservers1, Catalog: &catalog, SOAEditAPI: &soaEditApi1}}, nil}, - {"Non-existing ClusterZone", &dnsv1alpha2.ClusterZone{ObjectMeta: metav1.ObjectMeta{Name: name2}, Spec: dnsv1alpha2.ZoneSpec{Kind: MASTER_KIND_ZONE, Nameservers: nameservers2, Catalog: &catalog, SOAEditAPI: &soaEditApi2}}, nil}, - {"communication error", &dnsv1alpha2.Zone{ObjectMeta: metav1.ObjectMeta{Name: FAKE_SITE, Namespace: namespace1}, Spec: dnsv1alpha2.ZoneSpec{Kind: MASTER_KIND_ZONE, Nameservers: nameservers1, Catalog: &catalog, SOAEditAPI: &soaEditApi1}}, &powerdns.Error{StatusCode: 500, Status: "500 Internal Server Error", Message: "Internal Server Error"}}, + {"Valid Zone", &dnsv1alpha3.Zone{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace}, Spec: dnsv1alpha3.ZoneSpec{Kind: MASTER_KIND_ZONE, Nameservers: nameservers1, Catalog: &catalog, SOAEditAPI: &soaEditApi}}, nil}, + {"Non-existing Zone", &dnsv1alpha3.Zone{ObjectMeta: metav1.ObjectMeta{Name: name1, Namespace: namespace1}, Spec: dnsv1alpha3.ZoneSpec{Kind: MASTER_KIND_ZONE, Nameservers: nameservers1, Catalog: &catalog, SOAEditAPI: &soaEditApi1}}, nil}, + {"Non-existing ClusterZone", &dnsv1alpha3.ClusterZone{ObjectMeta: metav1.ObjectMeta{Name: name2}, Spec: dnsv1alpha3.ZoneSpec{Kind: MASTER_KIND_ZONE, Nameservers: nameservers2, Catalog: &catalog, SOAEditAPI: &soaEditApi2}}, nil}, + {"communication error", &dnsv1alpha3.Zone{ObjectMeta: metav1.ObjectMeta{Name: FAKE_SITE, Namespace: namespace1}, Spec: dnsv1alpha3.ZoneSpec{Kind: MASTER_KIND_ZONE, Nameservers: nameservers1, Catalog: &catalog, SOAEditAPI: &soaEditApi1}}, &powerdns.Error{StatusCode: 500, Status: "500 Internal Server Error", Message: "Internal Server Error"}}, } // Mock initialization @@ -318,12 +318,12 @@ func TestDeleteRrsetExternalResources(t *testing.T) { var testCases = []struct { description string - genericZone dnsv1alpha2.GenericZone - rrset *dnsv1alpha2.RRset + genericZone dnsv1alpha3.GenericZone + rrset *dnsv1alpha3.RRset e error }{ - {"Existing RRset", &dnsv1alpha2.Zone{ObjectMeta: metav1.ObjectMeta{Name: zoneName, Namespace: namespace}, Spec: dnsv1alpha2.ZoneSpec{Kind: MASTER_KIND_ZONE, Nameservers: nameservers1, Catalog: &catalog, SOAEditAPI: &soaEditApi}}, &dnsv1alpha2.RRset{ObjectMeta: metav1.ObjectMeta{Name: rrsetFqdn1, Namespace: namespace}, Spec: dnsv1alpha2.RRsetSpec{ZoneRef: dnsv1alpha2.ZoneRef{Name: zoneName, Kind: "Zone"}, Type: rrsetType1, Name: rrsetName1, TTL: rrsetTTL1, Records: rrsetRecords1, Comment: &rrsetComment1}}, nil}, - {"Inexisting RRset", &dnsv1alpha2.Zone{ObjectMeta: metav1.ObjectMeta{Name: zoneName, Namespace: namespace}, Spec: dnsv1alpha2.ZoneSpec{Kind: MASTER_KIND_ZONE, Nameservers: nameservers1, Catalog: &catalog, SOAEditAPI: &soaEditApi}}, &dnsv1alpha2.RRset{ObjectMeta: metav1.ObjectMeta{Name: rrsetFqdn2, Namespace: namespace}, Spec: dnsv1alpha2.RRsetSpec{ZoneRef: dnsv1alpha2.ZoneRef{Name: zoneName, Kind: "Zone"}, Type: rrsetType2, Name: rrsetName2, TTL: rrsetTTL2, Records: rrsetRecords2, Comment: &rrsetComment2}}, nil}, + {"Existing RRset", &dnsv1alpha3.Zone{ObjectMeta: metav1.ObjectMeta{Name: zoneName, Namespace: namespace}, Spec: dnsv1alpha3.ZoneSpec{Kind: MASTER_KIND_ZONE, Nameservers: nameservers1, Catalog: &catalog, SOAEditAPI: &soaEditApi}}, &dnsv1alpha3.RRset{ObjectMeta: metav1.ObjectMeta{Name: rrsetFqdn1, Namespace: namespace}, Spec: dnsv1alpha3.RRsetSpec{ZoneRef: dnsv1alpha3.ZoneRef{Name: zoneName, Kind: "Zone"}, Type: rrsetType1, Name: rrsetName1, TTL: rrsetTTL1, Records: rrsetRecords1, Comment: &rrsetComment1}}, nil}, + {"Inexisting RRset", &dnsv1alpha3.Zone{ObjectMeta: metav1.ObjectMeta{Name: zoneName, Namespace: namespace}, Spec: dnsv1alpha3.ZoneSpec{Kind: MASTER_KIND_ZONE, Nameservers: nameservers1, Catalog: &catalog, SOAEditAPI: &soaEditApi}}, &dnsv1alpha3.RRset{ObjectMeta: metav1.ObjectMeta{Name: rrsetFqdn2, Namespace: namespace}, Spec: dnsv1alpha3.RRsetSpec{ZoneRef: dnsv1alpha3.ZoneRef{Name: zoneName, Kind: "Zone"}, Type: rrsetType2, Name: rrsetName2, TTL: rrsetTTL2, Records: rrsetRecords2, Comment: &rrsetComment2}}, nil}, } // Mock initialization @@ -367,14 +367,14 @@ func TestCreateOrUpdateRrsetExternalResources(t *testing.T) { var testCases = []struct { description string - genericZone dnsv1alpha2.GenericZone - rrset *dnsv1alpha2.RRset + genericZone dnsv1alpha3.GenericZone + rrset *dnsv1alpha3.RRset want bool e error }{ - {"RRset creation", &dnsv1alpha2.Zone{ObjectMeta: metav1.ObjectMeta{Name: zoneName, Namespace: namespace}, Spec: dnsv1alpha2.ZoneSpec{Kind: MASTER_KIND_ZONE, Nameservers: nameservers1, Catalog: &catalog, SOAEditAPI: &soaEditApi}}, &dnsv1alpha2.RRset{ObjectMeta: metav1.ObjectMeta{Name: rrsetFqdn2, Namespace: namespace}, Spec: dnsv1alpha2.RRsetSpec{ZoneRef: dnsv1alpha2.ZoneRef{Name: zoneName, Kind: "Zone"}, Type: rrsetType2, Name: rrsetName2, TTL: rrsetTTL2, Records: rrsetRecords2, Comment: &rrsetComment2}}, true, nil}, - {"RRset update", &dnsv1alpha2.Zone{ObjectMeta: metav1.ObjectMeta{Name: zoneName, Namespace: namespace}, Spec: dnsv1alpha2.ZoneSpec{Kind: MASTER_KIND_ZONE, Nameservers: nameservers1, Catalog: &catalog, SOAEditAPI: &soaEditApi}}, &dnsv1alpha2.RRset{ObjectMeta: metav1.ObjectMeta{Name: rrsetFqdn1, Namespace: namespace}, Spec: dnsv1alpha2.RRsetSpec{ZoneRef: dnsv1alpha2.ZoneRef{Name: zoneName, Kind: "Zone"}, Type: rrsetType1, Name: rrsetName1, TTL: rrsetTTL1, Records: rrsetRecords1, Comment: &rrsetComment1}}, true, nil}, - {"RRset identical", &dnsv1alpha2.Zone{ObjectMeta: metav1.ObjectMeta{Name: zoneName, Namespace: namespace}, Spec: dnsv1alpha2.ZoneSpec{Kind: MASTER_KIND_ZONE, Nameservers: nameservers1, Catalog: &catalog, SOAEditAPI: &soaEditApi}}, &dnsv1alpha2.RRset{ObjectMeta: metav1.ObjectMeta{Name: rrsetFqdn1, Namespace: namespace}, Spec: dnsv1alpha2.RRsetSpec{ZoneRef: dnsv1alpha2.ZoneRef{Name: zoneName, Kind: "Zone"}, Type: rrsetType1, Name: rrsetName1, TTL: rrsetTTL1, Records: rrsetRecords1, Comment: &rrsetComment1}}, false, nil}, + {"RRset creation", &dnsv1alpha3.Zone{ObjectMeta: metav1.ObjectMeta{Name: zoneName, Namespace: namespace}, Spec: dnsv1alpha3.ZoneSpec{Kind: MASTER_KIND_ZONE, Nameservers: nameservers1, Catalog: &catalog, SOAEditAPI: &soaEditApi}}, &dnsv1alpha3.RRset{ObjectMeta: metav1.ObjectMeta{Name: rrsetFqdn2, Namespace: namespace}, Spec: dnsv1alpha3.RRsetSpec{ZoneRef: dnsv1alpha3.ZoneRef{Name: zoneName, Kind: "Zone"}, Type: rrsetType2, Name: rrsetName2, TTL: rrsetTTL2, Records: rrsetRecords2, Comment: &rrsetComment2}}, true, nil}, + {"RRset update", &dnsv1alpha3.Zone{ObjectMeta: metav1.ObjectMeta{Name: zoneName, Namespace: namespace}, Spec: dnsv1alpha3.ZoneSpec{Kind: MASTER_KIND_ZONE, Nameservers: nameservers1, Catalog: &catalog, SOAEditAPI: &soaEditApi}}, &dnsv1alpha3.RRset{ObjectMeta: metav1.ObjectMeta{Name: rrsetFqdn1, Namespace: namespace}, Spec: dnsv1alpha3.RRsetSpec{ZoneRef: dnsv1alpha3.ZoneRef{Name: zoneName, Kind: "Zone"}, Type: rrsetType1, Name: rrsetName1, TTL: rrsetTTL1, Records: rrsetRecords1, Comment: &rrsetComment1}}, true, nil}, + {"RRset identical", &dnsv1alpha3.Zone{ObjectMeta: metav1.ObjectMeta{Name: zoneName, Namespace: namespace}, Spec: dnsv1alpha3.ZoneSpec{Kind: MASTER_KIND_ZONE, Nameservers: nameservers1, Catalog: &catalog, SOAEditAPI: &soaEditApi}}, &dnsv1alpha3.RRset{ObjectMeta: metav1.ObjectMeta{Name: rrsetFqdn1, Namespace: namespace}, Spec: dnsv1alpha3.RRsetSpec{ZoneRef: dnsv1alpha3.ZoneRef{Name: zoneName, Kind: "Zone"}, Type: rrsetType1, Name: rrsetName1, TTL: rrsetTTL1, Records: rrsetRecords1, Comment: &rrsetComment1}}, false, nil}, } // Mock initialization diff --git a/internal/controller/pdns_client.go b/internal/controller/pdns_client.go new file mode 100644 index 0000000..01b068b --- /dev/null +++ b/internal/controller/pdns_client.go @@ -0,0 +1,196 @@ +/* + * Software Name : PowerDNS-Operator + * + * SPDX-FileCopyrightText: Copyright (c) PowerDNS-Operator contributors + * SPDX-License-Identifier: Apache-2.0 + * + * This software is distributed under the Apache 2.0 License, + * see the "LICENSE" file for more details + */ + +package controller + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "net/http" + "net/url" + "os" + + "github.com/joeig/go-powerdns/v3" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + dnsv1alpha3 "github.com/powerdns-operator/powerdns-operator/api/v1alpha3" +) + +// newPDNSClientFunc allows overriding the client creation for tests +var newPDNSClientFunc = newPDNSClientFromProvider + +// GetPDNSClient returns a PDNS client for the specified provider +func GetPDNSClient(ctx context.Context, kubeClient client.Client, providerRef string) (PdnsClienter, error) { + pdnsClient, err := newPDNSClientFunc(ctx, kubeClient, providerRef) + if err != nil { + return PdnsClienter{}, fmt.Errorf("failed to create PDNS client for provider '%s': %w", providerRef, err) + } + return pdnsClient, nil +} + +// newPDNSClientFromProvider creates a PDNS client from a PDNSProvider resource +func newPDNSClientFromProvider(ctx context.Context, kubeClient client.Client, providerName string) (PdnsClienter, error) { + // Validate pdnsprovider name + if providerName == "" { + return PdnsClienter{}, fmt.Errorf("pdnsprovider name cannot be empty") + } + + // Get PDNSProvider resource from Kubernetes + provider := &dnsv1alpha3.PDNSProvider{} + if err := kubeClient.Get(ctx, client.ObjectKey{Name: providerName}, provider); err != nil { + return PdnsClienter{}, fmt.Errorf("pdnsprovider '%s' not found: %w", providerName, err) + } + + // Validate provider configuration + if provider.Spec.URL == "" { + return PdnsClienter{}, fmt.Errorf("pdnsprovider '%s' has no API URL configured", providerName) + } + + // Get API key from Kubernetes secret + apiKey, err := extractAPIKey(ctx, kubeClient, provider) + if err != nil { + return PdnsClienter{}, fmt.Errorf("failed to get API key for pdnsprovider '%s': %w", providerName, err) + } + + // Configure HTTP client with TLS settings + httpClient, err := newHTTPClient(ctx, kubeClient, provider) + if err != nil { + return PdnsClienter{}, fmt.Errorf("failed to create HTTP client for pdnsprovider '%s': %w", providerName, err) + } + + // Create PowerDNS client using the go-powerdns library + powerDNSClient := powerdns.New(provider.Spec.URL, provider.GetVhost(), + powerdns.WithAPIKey(apiKey), powerdns.WithHTTPClient(httpClient)) + + return PdnsClienter{Records: powerDNSClient.Records, Zones: powerDNSClient.Zones}, nil +} + +func extractAPIKey(ctx context.Context, kubeClient client.Client, provider *dnsv1alpha3.PDNSProvider) (string, error) { + secret := &corev1.Secret{} + secretName := provider.GetCredentialsSecretName() + secretNamespace := provider.GetCredentialsSecretNamespace() + secretKey := provider.GetCredentialsSecretKey() + + if secretName == "" { + return "", fmt.Errorf("no secret reference configured") + } + + if err := kubeClient.Get(ctx, client.ObjectKey{ + Name: secretName, + Namespace: secretNamespace, + }, secret); err != nil { + return "", fmt.Errorf("failed to get secret '%s/%s': %w", secretNamespace, secretName, err) + } + + apiKey, exists := secret.Data[secretKey] + if !exists { + return "", fmt.Errorf("'%s' field not found in secret '%s/%s'", secretKey, secretNamespace, secretName) + } + + if len(apiKey) == 0 { + return "", fmt.Errorf("'%s' field is empty in secret '%s/%s'", secretKey, secretNamespace, secretName) + } + + return string(apiKey), nil +} + +func newHTTPClient(ctx context.Context, kubeClient client.Client, provider *dnsv1alpha3.PDNSProvider) (*http.Client, error) { + tlsConfig := &tls.Config{InsecureSkipVerify: provider.GetTLSInsecure()} + + // Handle CA certificate if provided via CA bundle reference + if provider.Spec.TLS != nil && provider.Spec.TLS.CABundleRef != nil { + caBundleData, err := getCABundleData(ctx, kubeClient, provider) + if err != nil { + return nil, fmt.Errorf("failed to get CA bundle: %w", err) + } + caCertPool := x509.NewCertPool() + if !caCertPool.AppendCertsFromPEM(caBundleData) { + return nil, fmt.Errorf("failed to parse CA certificate") + } + tlsConfig.RootCAs = caCertPool + } + + transport := &http.Transport{TLSClientConfig: tlsConfig} + + // Handle proxy configuration if provided + if provider.Spec.Proxy != nil && *provider.Spec.Proxy != "" { + proxyURL, err := url.Parse(*provider.Spec.Proxy) + if err != nil { + return nil, fmt.Errorf("failed to parse proxy URL '%s': %w", *provider.Spec.Proxy, err) + } + transport.Proxy = http.ProxyURL(proxyURL) + } + + return &http.Client{ + Transport: transport, + Timeout: provider.GetTimeout(), + }, nil +} + +func getCABundleData(ctx context.Context, kubeClient client.Client, provider *dnsv1alpha3.PDNSProvider) ([]byte, error) { + if provider.Spec.TLS == nil || provider.Spec.TLS.CABundleRef == nil { + return nil, fmt.Errorf("CA bundle reference is nil") + } + + caBundleRef := provider.Spec.TLS.CABundleRef + kind := provider.GetCABundleRefKind() + key := provider.GetCABundleRefKey() + + // Use the namespace from the CA bundle ref if specified, otherwise use operator namespace + // Note: PDNSProvider is cluster-scoped so it has no namespace + namespace := getOperatorNamespace() + if caBundleRef.Namespace != nil { + namespace = *caBundleRef.Namespace + } + + objKey := types.NamespacedName{ + Name: caBundleRef.Name, + Namespace: namespace, + } + + if kind == "Secret" { + secret := &corev1.Secret{} + err := kubeClient.Get(ctx, objKey, secret) + if err != nil { + return nil, fmt.Errorf("failed to get secret %s/%s: %w", objKey.Namespace, objKey.Name, err) + } + data, exists := secret.Data[key] + if !exists { + return nil, fmt.Errorf("%s not found in secret %s/%s", key, objKey.Namespace, objKey.Name) + } + return data, nil + } else { + configMap := &corev1.ConfigMap{} + err := kubeClient.Get(ctx, objKey, configMap) + if err != nil { + return nil, fmt.Errorf("failed to get configmap %s/%s: %w", objKey.Namespace, objKey.Name, err) + } + data, exists := configMap.Data[key] + if !exists { + return nil, fmt.Errorf("%s not found in configmap %s/%s", key, objKey.Namespace, objKey.Name) + } + return []byte(data), nil + } +} + +// getOperatorNamespace returns the operator's namespace +// It first tries to read from OPERATOR_NAMESPACE environment variable, +// then falls back to the default operator namespace +func getOperatorNamespace() string { + if ns := os.Getenv("OPERATOR_NAMESPACE"); ns != "" { + return ns + } + // Default operator namespace + return "powerdns-operator-system" +} diff --git a/internal/controller/pdns_client_test.go b/internal/controller/pdns_client_test.go new file mode 100644 index 0000000..c900043 --- /dev/null +++ b/internal/controller/pdns_client_test.go @@ -0,0 +1,373 @@ +/* + * Software Name : PowerDNS-Operator + * + * SPDX-FileCopyrightText: Copyright (c) PowerDNS-Operator contributors + * SPDX-License-Identifier: Apache-2.0 + * + * This software is distributed under the Apache 2.0 License, + * see the "LICENSE" file for more details + */ + +package controller + +import ( + "context" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + + dnsv1alpha3 "github.com/powerdns-operator/powerdns-operator/api/v1alpha3" +) + +var _ = Describe("PDNS Client Selection", func() { + var ( + ctx context.Context + originalNewPDNSClient func(context.Context, client.Client, string) (PdnsClienter, error) + testPDNSProviderName = "test-pdnsprovider" + testSecretName = "test-secret" + testSecretNamespace = "default" + testAPIKey = "test-api-key" + testAPIURL = "https://test-powerdns:8081" + ) + + BeforeEach(func() { + ctx = context.Background() + // Save the current function and restore the real implementation for these tests + originalNewPDNSClient = newPDNSClientFunc + newPDNSClientFunc = newPDNSClientFromProvider + }) + + AfterEach(func() { + // Restore the original function (which is the mock for other tests) + newPDNSClientFunc = originalNewPDNSClient + }) + + Context("GetPDNSClient function", func() { + It("should return pdns client when providerRef is provided and pdnsprovider exists", func() { + By("creating a test secret") + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: testSecretName, + Namespace: testSecretNamespace, + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "apiKey": []byte(testAPIKey), + }, + } + Expect(k8sClient.Create(ctx, secret)).To(Succeed()) + + By("creating a test pdnsprovider") + pdnsprovider := &dnsv1alpha3.PDNSProvider{ + ObjectMeta: metav1.ObjectMeta{ + Name: testPDNSProviderName, + }, + Spec: dnsv1alpha3.PDNSProviderSpec{ + URL: testAPIURL, + Credentials: dnsv1alpha3.PDNSProviderCredentials{ + SecretRef: dnsv1alpha3.PDNSProviderSecretRef{ + Name: testSecretName, + Namespace: ptr.To(testSecretNamespace), + }, + }, + Vhost: ptr.To("localhost"), + Timeout: ptr.To(metav1.Duration{Duration: 10 * time.Second}), + TLS: &dnsv1alpha3.PDNSProviderTLSConfig{ + Insecure: ptr.To(true), + }, + }, + Status: dnsv1alpha3.PDNSProviderStatus{ + ConnectionStatus: &[]string{"Connected"}[0], + }, + } + Expect(k8sClient.Create(ctx, pdnsprovider)).To(Succeed()) + + By("calling GetPDNSClient with pdnsprovider reference") + pdnsClient, err := GetPDNSClient(ctx, k8sClient, testPDNSProviderName) + + Expect(err).NotTo(HaveOccurred()) + Expect(pdnsClient.Records).NotTo(BeNil()) + Expect(pdnsClient.Zones).NotTo(BeNil()) + + By("cleaning up") + Expect(k8sClient.Delete(ctx, pdnsprovider)).To(Succeed()) + // Wait for pdnsprovider deletion to complete + Eventually(func() bool { + err := k8sClient.Get(ctx, client.ObjectKey{Name: pdnsprovider.Name}, &dnsv1alpha3.PDNSProvider{}) + return err != nil + }, time.Second*10, time.Millisecond*100).Should(BeTrue()) + + Expect(k8sClient.Delete(ctx, secret)).To(Succeed()) + // Wait for secret deletion to complete + Eventually(func() bool { + err := k8sClient.Get(ctx, client.ObjectKey{Name: secret.Name, Namespace: secret.Namespace}, &corev1.Secret{}) + return err != nil + }, time.Second*10, time.Millisecond*100).Should(BeTrue()) + }) + + It("should return error when pdnsprovider does not exist", func() { + nonExistentPDNSProvider := "non-existent-pdnsprovider" + By("calling GetPDNSClient with non-existent pdnsprovider") + _, err := GetPDNSClient(ctx, k8sClient, nonExistentPDNSProvider) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("pdnsprovider 'non-existent-pdnsprovider' not found")) + }) + + It("should return error when pdnsprovider exists but secret is missing", func() { + By("creating a pdnsprovider without secret") + pdnsprovider := &dnsv1alpha3.PDNSProvider{ + ObjectMeta: metav1.ObjectMeta{ + Name: testPDNSProviderName + "-missing-secret", + }, + Spec: dnsv1alpha3.PDNSProviderSpec{ + URL: testAPIURL, + Credentials: dnsv1alpha3.PDNSProviderCredentials{ + SecretRef: dnsv1alpha3.PDNSProviderSecretRef{ + Name: "missing-secret", + Namespace: ptr.To(testSecretNamespace), + }, + }, + }, + } + Expect(k8sClient.Create(ctx, pdnsprovider)).To(Succeed()) + + By("calling GetPDNSClient") + pdnsproviderName := testPDNSProviderName + "-missing-secret" + _, err := GetPDNSClient(ctx, k8sClient, pdnsproviderName) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to get secret")) + + By("cleaning up") + Expect(k8sClient.Delete(ctx, pdnsprovider)).To(Succeed()) + // Wait for deletion to complete + Eventually(func() bool { + err := k8sClient.Get(ctx, client.ObjectKey{Name: pdnsprovider.Name}, &dnsv1alpha3.PDNSProvider{}) + return err != nil + }, time.Second*10, time.Millisecond*100).Should(BeTrue()) + }) + + It("should return error when secret exists but apiKey is missing", func() { + By("creating a secret without apiKey") + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: testSecretName + "-no-apikey", + Namespace: testSecretNamespace, + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "wrongKey": []byte("some-value"), + }, + } + Expect(k8sClient.Create(ctx, secret)).To(Succeed()) + + By("creating a pdnsprovider") + pdnsprovider := &dnsv1alpha3.PDNSProvider{ + ObjectMeta: metav1.ObjectMeta{ + Name: testPDNSProviderName + "-no-apikey", + }, + Spec: dnsv1alpha3.PDNSProviderSpec{ + URL: testAPIURL, + Credentials: dnsv1alpha3.PDNSProviderCredentials{ + SecretRef: dnsv1alpha3.PDNSProviderSecretRef{ + Name: testSecretName + "-no-apikey", + Namespace: ptr.To(testSecretNamespace), + }, + }, + }, + } + Expect(k8sClient.Create(ctx, pdnsprovider)).To(Succeed()) + + By("calling GetPDNSClient") + pdnsproviderName := testPDNSProviderName + "-no-apikey" + _, err := GetPDNSClient(ctx, k8sClient, pdnsproviderName) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("'apiKey' field not found")) + + By("cleaning up") + Expect(k8sClient.Delete(ctx, pdnsprovider)).To(Succeed()) + // Wait for deletion to complete + Eventually(func() bool { + err := k8sClient.Get(ctx, client.ObjectKey{Name: pdnsprovider.Name}, &dnsv1alpha3.PDNSProvider{}) + return err != nil + }, time.Second*10, time.Millisecond*100).Should(BeTrue()) + + Expect(k8sClient.Delete(ctx, secret)).To(Succeed()) + // Wait for secret deletion to complete + Eventually(func() bool { + err := k8sClient.Get(ctx, client.ObjectKey{Name: secret.Name, Namespace: secret.Namespace}, &corev1.Secret{}) + return err != nil + }, time.Second*10, time.Millisecond*100).Should(BeTrue()) + }) + + It("should handle pdnsprovider with proxy URL", func() { + By("creating a secret with apiKey") + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "proxy-secret", + Namespace: testSecretNamespace, + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "apiKey": []byte(testAPIKey), + }, + } + Expect(k8sClient.Create(ctx, secret)).To(Succeed()) + + By("creating a pdnsprovider with proxy URL") + pdnsprovider := &dnsv1alpha3.PDNSProvider{ + ObjectMeta: metav1.ObjectMeta{ + Name: "proxy-pdnsprovider", + }, + Spec: dnsv1alpha3.PDNSProviderSpec{ + URL: testAPIURL, + Credentials: dnsv1alpha3.PDNSProviderCredentials{ + SecretRef: dnsv1alpha3.PDNSProviderSecretRef{ + Name: "proxy-secret", + Namespace: ptr.To(testSecretNamespace), + }, + }, + Vhost: ptr.To("localhost"), + Timeout: ptr.To(metav1.Duration{Duration: 10 * time.Second}), + TLS: &dnsv1alpha3.PDNSProviderTLSConfig{ + Insecure: ptr.To(true), + }, + Proxy: ptr.To("http://proxy.example.com:8080"), + }, + } + Expect(k8sClient.Create(ctx, pdnsprovider)).To(Succeed()) + + By("calling GetPDNSClient with proxy pdnsprovider") + proxyPDNSProviderName := "proxy-pdnsprovider" + pdnsClient, err := GetPDNSClient(ctx, k8sClient, proxyPDNSProviderName) + + Expect(err).NotTo(HaveOccurred()) + Expect(pdnsClient.Records).NotTo(BeNil()) + Expect(pdnsClient.Zones).NotTo(BeNil()) + + By("cleaning up") + Expect(k8sClient.Delete(ctx, pdnsprovider)).To(Succeed()) + // Wait for pdnsprovider deletion to complete + Eventually(func() bool { + err := k8sClient.Get(ctx, client.ObjectKey{Name: pdnsprovider.Name}, &dnsv1alpha3.PDNSProvider{}) + return err != nil + }, time.Second*10, time.Millisecond*100).Should(BeTrue()) + + Expect(k8sClient.Delete(ctx, secret)).To(Succeed()) + // Wait for secret deletion to complete + Eventually(func() bool { + err := k8sClient.Get(ctx, client.ObjectKey{Name: secret.Name, Namespace: secret.Namespace}, &corev1.Secret{}) + return err != nil + }, time.Second*10, time.Millisecond*100).Should(BeTrue()) + }) + + It("should handle pdnsprovider with empty API URL", func() { + By("creating a secret") + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "empty-url-secret", + Namespace: testSecretNamespace, + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "apiKey": []byte(testAPIKey), + }, + } + Expect(k8sClient.Create(ctx, secret)).To(Succeed()) + + By("attempting to create a pdnsprovider with empty API URL should fail validation") + pdnsprovider := &dnsv1alpha3.PDNSProvider{ + ObjectMeta: metav1.ObjectMeta{ + Name: "empty-url-pdnsprovider", + }, + Spec: dnsv1alpha3.PDNSProviderSpec{ + URL: "", // Empty URL + Credentials: dnsv1alpha3.PDNSProviderCredentials{ + SecretRef: dnsv1alpha3.PDNSProviderSecretRef{ + Name: "empty-url-secret", + Namespace: ptr.To(testSecretNamespace), + }, + }, + }, + } + + By("verifying that empty URL is rejected by CRD validation") + err := k8sClient.Create(ctx, pdnsprovider) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("spec.url in body should match")) + + By("cleaning up") + Expect(k8sClient.Delete(ctx, secret)).To(Succeed()) + }) + }) + + Context("Error handling for invalid configurations", func() { + It("should return error for invalid proxy URL", func() { + By("creating a secret with apiKey") + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "invalid-proxy-secret", + Namespace: testSecretNamespace, + }, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "apiKey": []byte(testAPIKey), + }, + } + Expect(k8sClient.Create(ctx, secret)).To(Succeed()) + + By("creating a pdnsprovider with invalid proxy URL") + pdnsprovider := &dnsv1alpha3.PDNSProvider{ + ObjectMeta: metav1.ObjectMeta{ + Name: "invalid-proxy-pdnsprovider", + }, + Spec: dnsv1alpha3.PDNSProviderSpec{ + URL: testAPIURL, + Credentials: dnsv1alpha3.PDNSProviderCredentials{ + SecretRef: dnsv1alpha3.PDNSProviderSecretRef{ + Name: "invalid-proxy-secret", + Namespace: ptr.To(testSecretNamespace), + }, + }, + Vhost: ptr.To("localhost"), + Timeout: ptr.To(metav1.Duration{Duration: 10 * time.Second}), + TLS: &dnsv1alpha3.PDNSProviderTLSConfig{ + Insecure: ptr.To(true), + }, + Proxy: ptr.To("://invalid-proxy-url"), // Invalid URL format + }, + } + Expect(k8sClient.Create(ctx, pdnsprovider)).To(Succeed()) + + By("calling GetPDNSClient should fail with proxy URL error") + invalidProxyPDNSProviderName := "invalid-proxy-pdnsprovider" + _, err := GetPDNSClient(ctx, k8sClient, invalidProxyPDNSProviderName) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to parse proxy URL")) + Expect(err.Error()).To(ContainSubstring("://invalid-proxy-url")) + + By("cleaning up") + Expect(k8sClient.Delete(ctx, pdnsprovider)).To(Succeed()) + // Wait for pdnsprovider deletion to complete + Eventually(func() bool { + err := k8sClient.Get(ctx, client.ObjectKey{Name: pdnsprovider.Name}, &dnsv1alpha3.PDNSProvider{}) + return err != nil + }, time.Second*10, time.Millisecond*100).Should(BeTrue()) + + Expect(k8sClient.Delete(ctx, secret)).To(Succeed()) + // Wait for secret deletion to complete + Eventually(func() bool { + err := k8sClient.Get(ctx, client.ObjectKey{Name: secret.Name, Namespace: secret.Namespace}, &corev1.Secret{}) + return err != nil + }, time.Second*10, time.Millisecond*100).Should(BeTrue()) + }) + }) +}) diff --git a/internal/controller/pdns_helper.go b/internal/controller/pdns_helper.go index 95ac4e4..32b1dc3 100644 --- a/internal/controller/pdns_helper.go +++ b/internal/controller/pdns_helper.go @@ -18,7 +18,7 @@ import ( "strings" "github.com/joeig/go-powerdns/v3" - dnsv1alpha2 "github.com/powerdns-operator/powerdns-operator/api/v1alpha2" + dnsv1alpha3 "github.com/powerdns-operator/powerdns-operator/api/v1alpha3" "k8s.io/utils/ptr" ) @@ -48,7 +48,7 @@ type PdnsClienter struct { // zoneIsIdenticalToExternalZone return True, True if respectively kind, soa_edit_api and catalog are identical // and nameservers are identical between Zone and External Resource -func zoneIsIdenticalToExternalZone(zone dnsv1alpha2.GenericZone, externalZone *powerdns.Zone, ns []string) (bool, bool) { +func zoneIsIdenticalToExternalZone(zone dnsv1alpha3.GenericZone, externalZone *powerdns.Zone, ns []string) (bool, bool) { zoneCatalog := makeCanonical(ptr.Deref(zone.GetSpec().Catalog, "")) externalZoneCatalog := ptr.Deref(externalZone.Catalog, "") zoneSOAEditAPI := ptr.Deref(zone.GetSpec().SOAEditAPI, "") @@ -57,7 +57,7 @@ func zoneIsIdenticalToExternalZone(zone dnsv1alpha2.GenericZone, externalZone *p } // rrsetIsIdenticalToExternalRRset return True if Comments, Name, Type, TTL and Records are identical between RRSet and External Resource -func rrsetIsIdenticalToExternalRRset(rrset dnsv1alpha2.GenericRRset, externalRecord powerdns.RRset) bool { +func rrsetIsIdenticalToExternalRRset(rrset dnsv1alpha3.GenericRRset, externalRecord powerdns.RRset) bool { commentsIdentical := true if len(externalRecord.Comments) != 0 { if rrset.GetSpec().Comment != nil { @@ -87,7 +87,7 @@ func makeCanonical(in string) string { return result } -func getRRsetName(rrset dnsv1alpha2.GenericRRset) string { +func getRRsetName(rrset dnsv1alpha3.GenericRRset) string { if !strings.HasSuffix(rrset.GetSpec().Name, ".") { return makeCanonical(rrset.GetSpec().Name + "." + rrset.GetSpec().ZoneRef.Name) } diff --git a/internal/controller/pdns_helper_test.go b/internal/controller/pdns_helper_test.go index 3d476c9..4f8fb43 100644 --- a/internal/controller/pdns_helper_test.go +++ b/internal/controller/pdns_helper_test.go @@ -16,7 +16,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/joeig/go-powerdns/v3" - dnsv1alpha2 "github.com/powerdns-operator/powerdns-operator/api/v1alpha2" + dnsv1alpha3 "github.com/powerdns-operator/powerdns-operator/api/v1alpha3" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/ptr" ) @@ -37,7 +37,7 @@ func TestZoneIsIdenticalToExternalZone(t *testing.T) { var testCases = []struct { description string - genericZone dnsv1alpha2.GenericZone + genericZone dnsv1alpha3.GenericZone externalZone *powerdns.Zone nameservers []string zonesIdentical bool @@ -45,12 +45,12 @@ func TestZoneIsIdenticalToExternalZone(t *testing.T) { }{ { "Identical Zones", - &dnsv1alpha2.Zone{ + &dnsv1alpha3.Zone{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, }, - Spec: dnsv1alpha2.ZoneSpec{ + Spec: dnsv1alpha3.ZoneSpec{ Kind: MASTER_KIND_ZONE, Nameservers: nameservers, Catalog: &catalog, @@ -70,12 +70,12 @@ func TestZoneIsIdenticalToExternalZone(t *testing.T) { }, { "Different Zones on NS", - &dnsv1alpha2.Zone{ + &dnsv1alpha3.Zone{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, }, - Spec: dnsv1alpha2.ZoneSpec{ + Spec: dnsv1alpha3.ZoneSpec{ Kind: MASTER_KIND_ZONE, Nameservers: nameservers, Catalog: &catalog, @@ -95,12 +95,12 @@ func TestZoneIsIdenticalToExternalZone(t *testing.T) { }, { "Different Zones on Kind", - &dnsv1alpha2.Zone{ + &dnsv1alpha3.Zone{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, }, - Spec: dnsv1alpha2.ZoneSpec{ + Spec: dnsv1alpha3.ZoneSpec{ Kind: NATIVE_KIND_ZONE, Nameservers: nameservers, Catalog: &catalog, @@ -120,12 +120,12 @@ func TestZoneIsIdenticalToExternalZone(t *testing.T) { }, { "Different Zones on Catalog", - &dnsv1alpha2.Zone{ + &dnsv1alpha3.Zone{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, }, - Spec: dnsv1alpha2.ZoneSpec{ + Spec: dnsv1alpha3.ZoneSpec{ Kind: NATIVE_KIND_ZONE, Nameservers: nameservers, Catalog: &catalog1, @@ -145,12 +145,12 @@ func TestZoneIsIdenticalToExternalZone(t *testing.T) { }, { "Different Zones on SOAEditAPI", - &dnsv1alpha2.Zone{ + &dnsv1alpha3.Zone{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, }, - Spec: dnsv1alpha2.ZoneSpec{ + Spec: dnsv1alpha3.ZoneSpec{ Kind: NATIVE_KIND_ZONE, Nameservers: nameservers, Catalog: &catalog, @@ -204,24 +204,24 @@ func TestRrsetIsIdenticalToExternalRRset(t *testing.T) { var testCases = []struct { description string - rrset *dnsv1alpha2.RRset + rrset *dnsv1alpha3.RRset externalRrset *powerdns.RRset rrsetsIdentical bool }{ { "Identical RRsets", - &dnsv1alpha2.RRset{ + &dnsv1alpha3.RRset{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, }, - Spec: dnsv1alpha2.RRsetSpec{ + Spec: dnsv1alpha3.RRsetSpec{ Comment: &recordComment1, Name: recordName, Type: recordType1, TTL: recordTtl1, Records: records, - ZoneRef: dnsv1alpha2.ZoneRef{ + ZoneRef: dnsv1alpha3.ZoneRef{ Name: zoneName, Kind: "Zone", }, @@ -253,18 +253,18 @@ func TestRrsetIsIdenticalToExternalRRset(t *testing.T) { }, { "Different RRsets on Comment", - &dnsv1alpha2.RRset{ + &dnsv1alpha3.RRset{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, }, - Spec: dnsv1alpha2.RRsetSpec{ + Spec: dnsv1alpha3.RRsetSpec{ Comment: &recordComment1, Name: recordName, Type: recordType1, TTL: recordTtl1, Records: records, - ZoneRef: dnsv1alpha2.ZoneRef{ + ZoneRef: dnsv1alpha3.ZoneRef{ Name: zoneName, Kind: "Zone", }, @@ -296,18 +296,18 @@ func TestRrsetIsIdenticalToExternalRRset(t *testing.T) { }, { "Different RRsets on Records", - &dnsv1alpha2.RRset{ + &dnsv1alpha3.RRset{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, }, - Spec: dnsv1alpha2.RRsetSpec{ + Spec: dnsv1alpha3.RRsetSpec{ Comment: &recordComment1, Name: recordName, Type: recordType1, TTL: recordTtl1, Records: records, - ZoneRef: dnsv1alpha2.ZoneRef{ + ZoneRef: dnsv1alpha3.ZoneRef{ Name: zoneName, Kind: "Zone", }, @@ -334,18 +334,18 @@ func TestRrsetIsIdenticalToExternalRRset(t *testing.T) { }, { "Different RRsets on Type", - &dnsv1alpha2.RRset{ + &dnsv1alpha3.RRset{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, }, - Spec: dnsv1alpha2.RRsetSpec{ + Spec: dnsv1alpha3.RRsetSpec{ Comment: &recordComment1, Name: recordName, Type: recordType1, TTL: recordTtl1, Records: records, - ZoneRef: dnsv1alpha2.ZoneRef{ + ZoneRef: dnsv1alpha3.ZoneRef{ Name: zoneName, Kind: "Zone", }, @@ -377,18 +377,18 @@ func TestRrsetIsIdenticalToExternalRRset(t *testing.T) { }, { "Different RRsets on TTL", - &dnsv1alpha2.RRset{ + &dnsv1alpha3.RRset{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, }, - Spec: dnsv1alpha2.RRsetSpec{ + Spec: dnsv1alpha3.RRsetSpec{ Comment: &recordComment1, Name: recordName, Type: recordType1, TTL: recordTtl1, Records: records, - ZoneRef: dnsv1alpha2.ZoneRef{ + ZoneRef: dnsv1alpha3.ZoneRef{ Name: zoneName, Kind: "Zone", }, @@ -474,23 +474,23 @@ func TestGetRRsetName(t *testing.T) { ) var testCases = []struct { description string - entry *dnsv1alpha2.RRset + entry *dnsv1alpha3.RRset want string }{ { "Non FQDN entry", - &dnsv1alpha2.RRset{ + &dnsv1alpha3.RRset{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, }, - Spec: dnsv1alpha2.RRsetSpec{ + Spec: dnsv1alpha3.RRsetSpec{ Comment: &recordComment, Name: recordName, Type: recordType, TTL: recordTtl, Records: records, - ZoneRef: dnsv1alpha2.ZoneRef{ + ZoneRef: dnsv1alpha3.ZoneRef{ Name: zoneName, Kind: "Zone", }, @@ -500,18 +500,18 @@ func TestGetRRsetName(t *testing.T) { }, { "FQDN entry", - &dnsv1alpha2.RRset{ + &dnsv1alpha3.RRset{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, }, - Spec: dnsv1alpha2.RRsetSpec{ + Spec: dnsv1alpha3.RRsetSpec{ Comment: &recordComment, Name: recordFQDName, Type: recordType, TTL: recordTtl, Records: records, - ZoneRef: dnsv1alpha2.ZoneRef{ + ZoneRef: dnsv1alpha3.ZoneRef{ Name: zoneName, Kind: "Zone", }, diff --git a/internal/controller/pdns_metrics.go b/internal/controller/pdns_metrics.go index c6d0202..745a406 100644 --- a/internal/controller/pdns_metrics.go +++ b/internal/controller/pdns_metrics.go @@ -1,7 +1,7 @@ package controller import ( - dnsv1alpha2 "github.com/powerdns-operator/powerdns-operator/api/v1alpha2" + dnsv1alpha3 "github.com/powerdns-operator/powerdns-operator/api/v1alpha3" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/testutil" ) @@ -37,9 +37,9 @@ var ( ) ) -func updateRrsetsMetrics(fqdn string, gr dnsv1alpha2.GenericRRset) { +func updateRrsetsMetrics(fqdn string, gr dnsv1alpha3.GenericRRset) { switch gr.(type) { - case *dnsv1alpha2.RRset: + case *dnsv1alpha3.RRset: rrsetsStatusesMetric.With(map[string]string{ "fqdn": fqdn, "type": gr.GetSpec().Type, @@ -48,7 +48,7 @@ func updateRrsetsMetrics(fqdn string, gr dnsv1alpha2.GenericRRset) { "namespace": gr.GetNamespace(), }).Set(1) - case *dnsv1alpha2.ClusterRRset: + case *dnsv1alpha3.ClusterRRset: clusterRrsetsStatusesMetric.With(map[string]string{ "fqdn": fqdn, "type": gr.GetSpec().Type, @@ -58,9 +58,9 @@ func updateRrsetsMetrics(fqdn string, gr dnsv1alpha2.GenericRRset) { } } -func removeRrsetMetrics(gr dnsv1alpha2.GenericRRset) { +func removeRrsetMetrics(gr dnsv1alpha3.GenericRRset) { switch gr.(type) { - case *dnsv1alpha2.RRset: + case *dnsv1alpha3.RRset: rrsetsStatusesMetric.DeletePartialMatch( map[string]string{ "namespace": gr.GetNamespace(), @@ -75,31 +75,31 @@ func removeRrsetMetrics(gr dnsv1alpha2.GenericRRset) { } } -func updateZonesMetrics(gz dnsv1alpha2.GenericZone) { +func updateZonesMetrics(gz dnsv1alpha3.GenericZone) { switch gz.(type) { - case *dnsv1alpha2.Zone: + case *dnsv1alpha3.Zone: zonesStatusesMetric.With(map[string]string{ "status": *gz.GetStatus().SyncStatus, "name": gz.GetName(), "namespace": gz.GetNamespace(), }).Set(1) - case *dnsv1alpha2.ClusterZone: + case *dnsv1alpha3.ClusterZone: clusterZonesStatusesMetric.With(map[string]string{ "status": *gz.GetStatus().SyncStatus, "name": gz.GetName(), }).Set(1) } } -func removeZonesMetrics(gz dnsv1alpha2.GenericZone) { +func removeZonesMetrics(gz dnsv1alpha3.GenericZone) { switch gz.(type) { - case *dnsv1alpha2.Zone: + case *dnsv1alpha3.Zone: zonesStatusesMetric.DeletePartialMatch( map[string]string{ "namespace": gz.GetNamespace(), "name": gz.GetName(), }, ) - case *dnsv1alpha2.ClusterZone: + case *dnsv1alpha3.ClusterZone: clusterZonesStatusesMetric.DeletePartialMatch( map[string]string{ "name": gz.GetName(), diff --git a/internal/controller/pdnsprovider_controller.go b/internal/controller/pdnsprovider_controller.go new file mode 100644 index 0000000..7bbb4c5 --- /dev/null +++ b/internal/controller/pdnsprovider_controller.go @@ -0,0 +1,289 @@ +/* + * Software Name : PowerDNS-Operator + * + * SPDX-FileCopyrightText: Copyright (c) PowerDNS-Operator contributors + * SPDX-License-Identifier: Apache-2.0 + * + * This software is distributed under the Apache 2.0 License, + * see the "LICENSE" file for more details + */ + +package controller + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "net/http" + "net/url" + + "time" + + "github.com/joeig/go-powerdns/v3" + dnsv1alpha3 "github.com/powerdns-operator/powerdns-operator/api/v1alpha3" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +const ( + PDNSProviderReasonConnected = "Connected" + PDNSProviderMessageConnected = "Successfully connected to PowerDNS API" + PDNSProviderReasonConnectionFailed = "ConnectionFailed" + PDNSProviderReasonSecretNotFound = "SecretNotFound" + PDNSProviderMessageSecretNotFound = "Referenced secret not found" +) + +// PDNSProviderReconciler reconciles a PDNSProvider object +type PDNSProviderReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +// +kubebuilder:rbac:groups=dns.cav.enablers.ob,resources=pdnsproviders,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=dns.cav.enablers.ob,resources=pdnsproviders/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=dns.cav.enablers.ob,resources=pdnsproviders/finalizers,verbs=update +// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch + +func (r *PDNSProviderReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := log.FromContext(ctx) + log.Info("Reconcile PDNSProvider", "PDNSProvider.Name", req.Name) + + // Get PDNSProvider + pdnsprovider := &dnsv1alpha3.PDNSProvider{} + err := r.Get(ctx, req.NamespacedName, pdnsprovider) + if err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + // Initialize variables + isDeleted := !pdnsprovider.DeletionTimestamp.IsZero() + + // Handle finalizer + if !isDeleted { + if !controllerutil.ContainsFinalizer(pdnsprovider, RESOURCES_FINALIZER_NAME) { + controllerutil.AddFinalizer(pdnsprovider, RESOURCES_FINALIZER_NAME) + if err := r.Update(ctx, pdnsprovider); err != nil { + log.Error(err, "Failed to add finalizer") + return ctrl.Result{}, err + } + } + } else { + if controllerutil.ContainsFinalizer(pdnsprovider, RESOURCES_FINALIZER_NAME) { + controllerutil.RemoveFinalizer(pdnsprovider, RESOURCES_FINALIZER_NAME) + if err := r.Update(ctx, pdnsprovider); err != nil { + log.Error(err, "Failed to remove finalizer") + return ctrl.Result{}, err + } + } + return ctrl.Result{}, nil + } + + return r.reconcilePDNSProvider(ctx, pdnsprovider) +} + +func (r *PDNSProviderReconciler) reconcilePDNSProvider(ctx context.Context, pdnsprovider *dnsv1alpha3.PDNSProvider) (ctrl.Result, error) { + log := log.FromContext(ctx) + + // Get API key from secret + apiKey, err := r.getAPIKeyFromSecret(ctx, pdnsprovider) + if err != nil { + log.Error(err, "Failed to get API key from secret") + r.updatePDNSProviderStatus(ctx, pdnsprovider, "Failed", nil, PDNSProviderReasonSecretNotFound, err.Error()) + return ctrl.Result{RequeueAfter: 30 * time.Second}, nil + } + + // Check PowerDNS connection + serverInfo, err := r.checkPowerDNSConnection(ctx, pdnsprovider, apiKey) + if err != nil { + log.Error(err, "Failed to connect to PowerDNS") + r.updatePDNSProviderStatus(ctx, pdnsprovider, "Failed", nil, PDNSProviderReasonConnectionFailed, err.Error()) + return ctrl.Result{RequeueAfter: 30 * time.Second}, nil + } + + // Update status with success + r.updatePDNSProviderStatus(ctx, pdnsprovider, "Connected", serverInfo, PDNSProviderReasonConnected, PDNSProviderMessageConnected) + return ctrl.Result{RequeueAfter: pdnsprovider.GetInterval()}, nil +} + +func (r *PDNSProviderReconciler) getAPIKeyFromSecret(ctx context.Context, pdnsprovider *dnsv1alpha3.PDNSProvider) (string, error) { + secret := &corev1.Secret{} + secretKey := types.NamespacedName{ + Name: pdnsprovider.GetCredentialsSecretName(), + Namespace: pdnsprovider.GetCredentialsSecretNamespace(), + } + + err := r.Get(ctx, secretKey, secret) + if err != nil { + return "", fmt.Errorf("failed to get secret %s/%s: %w", secretKey.Namespace, secretKey.Name, err) + } + + keyName := pdnsprovider.GetCredentialsSecretKey() + apiKey, exists := secret.Data[keyName] + if !exists { + return "", fmt.Errorf("%s not found in secret %s/%s", keyName, secretKey.Namespace, secretKey.Name) + } + + return string(apiKey), nil +} + +func (r *PDNSProviderReconciler) checkPowerDNSConnection(ctx context.Context, pdnsprovider *dnsv1alpha3.PDNSProvider, apiKey string) (map[string]*string, error) { + // Create HTTP client + tlsConfig := &tls.Config{InsecureSkipVerify: pdnsprovider.GetTLSInsecure()} + + // Handle CA bundle if specified + if pdnsprovider.Spec.TLS != nil && pdnsprovider.Spec.TLS.CABundleRef != nil { + caBundleData, err := r.getCABundleData(ctx, pdnsprovider) + if err != nil { + return nil, fmt.Errorf("failed to get CA bundle: %w", err) + } + caCertPool := x509.NewCertPool() + if !caCertPool.AppendCertsFromPEM(caBundleData) { + return nil, fmt.Errorf("failed to parse CA certificate") + } + tlsConfig.RootCAs = caCertPool + } + + transport := &http.Transport{TLSClientConfig: tlsConfig} + + // Configure proxy if specified + if pdnsprovider.Spec.Proxy != nil && *pdnsprovider.Spec.Proxy != "" { + proxyURL, err := url.Parse(*pdnsprovider.Spec.Proxy) + if err != nil { + return nil, fmt.Errorf("failed to parse proxy URL: %w", err) + } + transport.Proxy = http.ProxyURL(proxyURL) + } + + httpClient := &http.Client{ + Transport: transport, + Timeout: pdnsprovider.GetTimeout(), + } + + // Create PowerDNS client and check connection + pdnsClient := powerdns.New(pdnsprovider.Spec.URL, pdnsprovider.GetVhost(), + powerdns.WithAPIKey(apiKey), powerdns.WithHTTPClient(httpClient)) + + timeoutCtx, cancel := context.WithTimeout(ctx, pdnsprovider.GetTimeout()) + defer cancel() + + server, err := pdnsClient.Servers.Get(timeoutCtx, pdnsprovider.GetVhost()) + if err != nil { + return nil, fmt.Errorf("failed to connect to PowerDNS: %w", err) + } + + // Validate authoritative server + if server.DaemonType != nil && *server.DaemonType != "authoritative" { + return nil, fmt.Errorf("PowerDNS server is not authoritative, got: %s", *server.DaemonType) + } + + return map[string]*string{ + "version": server.Version, + "daemonType": server.DaemonType, + "serverID": server.ID, + }, nil +} + +func (r *PDNSProviderReconciler) getCABundleData(ctx context.Context, pdnsprovider *dnsv1alpha3.PDNSProvider) ([]byte, error) { + if pdnsprovider.Spec.TLS == nil || pdnsprovider.Spec.TLS.CABundleRef == nil { + return nil, fmt.Errorf("CA bundle reference is nil") + } + + caBundleRef := pdnsprovider.Spec.TLS.CABundleRef + kind := pdnsprovider.GetCABundleRefKind() + key := pdnsprovider.GetCABundleRefKey() + namespace := pdnsprovider.GetCABundleRefNamespace() + + objKey := types.NamespacedName{ + Name: caBundleRef.Name, + Namespace: namespace, + } + + if kind == "Secret" { + secret := &corev1.Secret{} + err := r.Get(ctx, objKey, secret) + if err != nil { + return nil, fmt.Errorf("failed to get secret %s/%s: %w", objKey.Namespace, objKey.Name, err) + } + data, exists := secret.Data[key] + if !exists { + return nil, fmt.Errorf("%s not found in secret %s/%s", key, objKey.Namespace, objKey.Name) + } + return data, nil + } else { + configMap := &corev1.ConfigMap{} + err := r.Get(ctx, objKey, configMap) + if err != nil { + return nil, fmt.Errorf("failed to get configmap %s/%s: %w", objKey.Namespace, objKey.Name, err) + } + data, exists := configMap.Data[key] + if !exists { + return nil, fmt.Errorf("%s not found in configmap %s/%s", key, objKey.Namespace, objKey.Name) + } + return []byte(data), nil + } +} + +func (r *PDNSProviderReconciler) updatePDNSProviderStatus(ctx context.Context, pdnsprovider *dnsv1alpha3.PDNSProvider, + status string, serverInfo map[string]*string, reason, message string) { + + original := pdnsprovider.DeepCopy() + now := metav1.NewTime(time.Now()) + + // Update basic status + pdnsprovider.Status.ConnectionStatus = &status + pdnsprovider.Status.ObservedGeneration = &pdnsprovider.Generation + + // Update server info if available + if serverInfo != nil { + pdnsprovider.Status.PowerDNSVersion = serverInfo["version"] + pdnsprovider.Status.DaemonType = serverInfo["daemonType"] + pdnsprovider.Status.ServerID = serverInfo["serverID"] + + // Only update LastConnectionTime if connection status changed or significant time passed + shouldUpdateTime := pdnsprovider.Status.ConnectionStatus == nil || + *pdnsprovider.Status.ConnectionStatus != status || + pdnsprovider.Status.LastConnectionTime == nil || + time.Since(pdnsprovider.Status.LastConnectionTime.Time) > 4*time.Minute + + if shouldUpdateTime { + pdnsprovider.Status.LastConnectionTime = &now + } + } + + // Update condition + conditionStatus := metav1.ConditionTrue + if status != "Connected" { + conditionStatus = metav1.ConditionFalse + } + + meta.SetStatusCondition(&pdnsprovider.Status.Conditions, metav1.Condition{ + Type: "Ready", + Status: conditionStatus, + LastTransitionTime: now, + Reason: reason, + Message: message, + ObservedGeneration: pdnsprovider.Generation, + }) + + if err := r.Status().Patch(ctx, pdnsprovider, client.MergeFrom(original)); err != nil { + log := log.FromContext(ctx) + log.Error(err, "Failed to update pdnsprovider status") + } +} + +// SetupWithManager sets up the controller with the Manager. +func (r *PDNSProviderReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&dnsv1alpha3.PDNSProvider{}). + Owns(&dnsv1alpha3.ClusterZone{}). + Owns(&dnsv1alpha3.Zone{}). + Complete(r) +} diff --git a/internal/controller/pdnsprovider_controller_test.go b/internal/controller/pdnsprovider_controller_test.go new file mode 100644 index 0000000..538c4e8 --- /dev/null +++ b/internal/controller/pdnsprovider_controller_test.go @@ -0,0 +1,157 @@ +/* + * Software Name : PowerDNS-Operator + * + * SPDX-FileCopyrightText: Copyright (c) PowerDNS-Operator contributors + * SPDX-FileCopyrightText: Copyright (c) 2025 Orange Business Services SA + * SPDX-License-Identifier: Apache-2.0 + * + * This software is distributed under the Apache 2.0 License, + * see the "LICENSE" file for more details + */ + +//nolint:goconst +package controller + +import ( + "context" + "strings" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + dnsv1alpha3 "github.com/powerdns-operator/powerdns-operator/api/v1alpha3" +) + +var _ = Describe("PDNSProvider Controller", func() { + + const ( + resourceName = "test-pdnsprovider" + resourceURL = "http://localhost:8081/api/v1" + resourceSecretRef = "test-powerdns-secret" + resourceNamespace = "default" + resourceAPIKeyRef = "apikey" + + timeout = time.Second * 5 + interval = time.Millisecond * 250 + ) + + typeNamespacedName := types.NamespacedName{ + Name: resourceName, + // PDNSProvider is cluster-scoped, so no namespace + } + + Context("When reconciling a resource", func() { + BeforeEach(func() { + ctx := context.Background() + By("creating the PDNSProvider resource") + resource := &dnsv1alpha3.PDNSProvider{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + }, + } + resource.SetResourceVersion("") + _, err := controllerutil.CreateOrUpdate(ctx, k8sClient, resource, func() error { + resource.Spec = dnsv1alpha3.PDNSProviderSpec{ + URL: resourceURL, + Credentials: dnsv1alpha3.PDNSProviderCredentials{ + SecretRef: dnsv1alpha3.PDNSProviderSecretRef{ + Name: resourceSecretRef, + Namespace: ptr.To(resourceNamespace), + Key: ptr.To(resourceAPIKeyRef), + }, + }, + } + return nil + }) + Expect(err).NotTo(HaveOccurred()) + Eventually(func() bool { + err := k8sClient.Get(ctx, typeNamespacedName, resource) + return err == nil + }, timeout, interval).Should(BeTrue()) + }) + + AfterEach(func() { + ctx := context.Background() + By("Cleanup the specific resource instance PDNSProvider") + resource := &dnsv1alpha3.PDNSProvider{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + if err == nil { + // Resource exists, delete it + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + Eventually(func() bool { + err := k8sClient.Get(ctx, typeNamespacedName, resource) + return errors.IsNotFound(err) + }, timeout, interval).Should(BeTrue()) + } else if !errors.IsNotFound(err) { + // Unexpected error + Expect(err).NotTo(HaveOccurred()) + } + // If resource is already deleted (NotFound), nothing to do + }) + It("should successfully reconcile the resource", func() { + ctx := context.Background() + By("Reconciling the created resource") + controllerReconciler := &PDNSProviderReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + // First reconcile might fail due to finalizer addition, retry if needed + const retryDelay = 100 * time.Millisecond + result, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + if err != nil { + // If the error is about object modification, retry once + if strings.Contains(err.Error(), "the object has been modified") { + // Wait briefly before retry to avoid race conditions + time.Sleep(retryDelay) + result, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ + NamespacedName: typeNamespacedName, + }) + } + } + Expect(err).NotTo(HaveOccurred()) + + By("Verifying the resource was processed correctly") + updatedProvider := &dnsv1alpha3.PDNSProvider{} + Eventually(func() bool { + err := k8sClient.Get(ctx, typeNamespacedName, updatedProvider) + return err == nil && controllerutil.ContainsFinalizer(updatedProvider, RESOURCES_FINALIZER_NAME) + }, timeout, interval).Should(BeTrue()) + + By("Verifying the resource status is updated") + Eventually(func() bool { + err := k8sClient.Get(ctx, typeNamespacedName, updatedProvider) + return err == nil && updatedProvider.Status.Conditions != nil && len(updatedProvider.Status.Conditions) > 0 + }, timeout, interval).Should(BeTrue()) + + _ = result // Use the result to avoid unused variable warnings + }) + + It("should handle resource deletion", func() { + ctx := context.Background() + By("Getting the resource") + resource := &dnsv1alpha3.PDNSProvider{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Deleting the resource") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + + By("Verifying the resource is deleted") + Eventually(func() bool { + err := k8sClient.Get(ctx, typeNamespacedName, resource) + return errors.IsNotFound(err) + }, timeout, interval).Should(BeTrue()) + }) + }) +}) diff --git a/internal/controller/rrset_controller.go b/internal/controller/rrset_controller.go index 9555fb9..63cfafc 100644 --- a/internal/controller/rrset_controller.go +++ b/internal/controller/rrset_controller.go @@ -28,7 +28,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/metrics" - dnsv1alpha2 "github.com/powerdns-operator/powerdns-operator/api/v1alpha2" + dnsv1alpha3 "github.com/powerdns-operator/powerdns-operator/api/v1alpha3" ) const ( @@ -45,8 +45,7 @@ const ( // RRsetReconciler reconciles a RRset object type RRsetReconciler struct { client.Client - Scheme *runtime.Scheme - PDNSClient PdnsClienter + Scheme *runtime.Scheme } func init() { @@ -63,7 +62,7 @@ func (r *RRsetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl log.Info("Reconcile RRset", "Zone.RRset.Name", req.Name) // RRset - rrset := &dnsv1alpha2.RRset{} + rrset := &dnsv1alpha3.RRset{} err := r.Get(ctx, req.NamespacedName, rrset) if err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) @@ -104,14 +103,14 @@ func (r *RRsetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl } // Zone - var zone dnsv1alpha2.GenericZone + var zone dnsv1alpha3.GenericZone switch rrset.Spec.ZoneRef.Kind { //nolint:goconst case "Zone": - zone = &dnsv1alpha2.Zone{} + zone = &dnsv1alpha3.Zone{} //nolint:goconst case "ClusterZone": - zone = &dnsv1alpha2.ClusterZone{} + zone = &dnsv1alpha3.ClusterZone{} } err = r.Get(ctx, client.ObjectKey{Namespace: rrset.Namespace, Name: rrset.Spec.ZoneRef.Name}, zone) if err != nil { @@ -166,6 +165,20 @@ func (r *RRsetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl // If a Zone/ClusterZone exists but is in Failed Status zoneIsInFailedStatus := (zone.GetStatus().SyncStatus != nil && *zone.GetStatus().SyncStatus == FAILED_STATUS) if zoneIsInFailedStatus { + // Check if we should retry based on last failure time of the RRset itself (not the zone) + // This prevents the RRset from being stuck if the zone status is stale or hasn't been updated + rrsetLastTransition := getLastRRsetConditionTransition(rrset) + timeSinceLastFailure := time.Since(rrsetLastTransition) + + // Only skip if the RRset failure is very recent (less than 30 seconds) + // This prevents excessive retries while still allowing recovery when parent zone recovers + if timeSinceLastFailure < 30*time.Second && !isModified { + // Update metrics and requeue + updateRrsetsMetrics(getRRsetName(rrset), rrset) + return ctrl.Result{RequeueAfter: 30 * time.Second}, nil + } + + // If enough time has passed or RRset was modified, update status but continue to try original = rrset.DeepCopy() rrset.Status.SyncStatus = ptr.To(FAILED_STATUS) rrset.Status.ObservedGeneration = &rrset.Generation @@ -194,28 +207,39 @@ func (r *RRsetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl return ctrl.Result{}, err } } + return ctrl.Result{}, nil } - return ctrl.Result{}, nil + // Continue with full reconciliation attempt despite parent zone being in Failed state + // This allows the RRset to recover if the parent zone has actually recovered but its status + // hasn't been updated yet, or if enough time has passed to warrant a retry attempt. + // We fall through to the normal PowerDNS client creation and reconciliation logic below. + } + + // Get the appropriate PowerDNS client + pdnsClient, err := GetPDNSClient(ctx, r.Client, zone.GetSpec().ProviderRef) + if err != nil { + log.Error(err, "Failed to get PowerDNS client") + return ctrl.Result{}, err } - return rrsetReconcile(ctx, rrset, zone, isModified, isDeleted, lastUpdateTime, r.Scheme, r.Client, r.PDNSClient, log) + return rrsetReconcile(ctx, rrset, zone, isModified, isDeleted, lastUpdateTime, r.Scheme, r.Client, pdnsClient, log) } // SetupWithManager sets up the controller with the Manager. func (r *RRsetReconciler) SetupWithManager(mgr ctrl.Manager) error { // We use indexer to ensure that only one RRset exists for DNS entry - if err := mgr.GetFieldIndexer().IndexField(context.Background(), &dnsv1alpha2.RRset{}, "RRset.Entry.Name", func(rawObj client.Object) []string { + if err := mgr.GetFieldIndexer().IndexField(context.Background(), &dnsv1alpha3.RRset{}, "RRset.Entry.Name", func(rawObj client.Object) []string { // grab the RRset object, extract its name... var RRsetName string - if rawObj.(*dnsv1alpha2.RRset).Status.SyncStatus == nil || *rawObj.(*dnsv1alpha2.RRset).Status.SyncStatus == SUCCEEDED_STATUS { - RRsetName = getRRsetName(rawObj.(*dnsv1alpha2.RRset)) + "/" + rawObj.(*dnsv1alpha2.RRset).Spec.Type + if rawObj.(*dnsv1alpha3.RRset).Status.SyncStatus == nil || *rawObj.(*dnsv1alpha3.RRset).Status.SyncStatus == SUCCEEDED_STATUS { + RRsetName = getRRsetName(rawObj.(*dnsv1alpha3.RRset)) + "/" + rawObj.(*dnsv1alpha3.RRset).Spec.Type } return []string{RRsetName} }); err != nil { return err } return ctrl.NewControllerManagedBy(mgr). - For(&dnsv1alpha2.RRset{}). + For(&dnsv1alpha3.RRset{}). Complete(r) } diff --git a/internal/controller/rrset_controller_test.go b/internal/controller/rrset_controller_test.go index cbf947d..8a64fcd 100644 --- a/internal/controller/rrset_controller_test.go +++ b/internal/controller/rrset_controller_test.go @@ -25,18 +25,19 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - dnsv1alpha2 "github.com/powerdns-operator/powerdns-operator/api/v1alpha2" + dnsv1alpha3 "github.com/powerdns-operator/powerdns-operator/api/v1alpha3" ) var _ = Describe("RRset Controller", func() { const ( // Zone - zoneName = "example2.org" - zoneNamespace = "example2" - zoneKind = NATIVE_KIND_ZONE - zoneNS1 = "ns1.example2.org" - zoneNS2 = "ns2.example2.org" + zoneName = "example2.org" + zoneNamespace = "example2" + zoneKind = NATIVE_KIND_ZONE + zoneProviderRef = "test-powerdns" + zoneNS1 = "ns1.example2.org" + zoneNS2 = "ns2.example2.org" // RRset resourceName = "test.example2.org" @@ -73,7 +74,7 @@ var _ = Describe("RRset Controller", func() { BeforeEach(func() { ctx := context.Background() By("Creating the Zone resource") - zone := &dnsv1alpha2.Zone{ + zone := &dnsv1alpha3.Zone{ ObjectMeta: metav1.ObjectMeta{ Name: zoneName, Namespace: zoneNamespace, @@ -81,7 +82,8 @@ var _ = Describe("RRset Controller", func() { } zone.SetResourceVersion("") _, err := controllerutil.CreateOrUpdate(ctx, k8sClient, zone, func() error { - zone.Spec = dnsv1alpha2.ZoneSpec{ + zone.Spec = dnsv1alpha3.ZoneSpec{ + ProviderRef: zoneProviderRef, Kind: zoneKind, Nameservers: []string{zoneNS1, zoneNS2}, } @@ -99,12 +101,12 @@ var _ = Describe("RRset Controller", func() { }, timeout, interval).Should(BeTrue()) By("Ensuring the resource does not already exists") - emptyResource := &dnsv1alpha2.RRset{} + emptyResource := &dnsv1alpha3.RRset{} err = k8sClient.Get(ctx, rssetLookupKey, emptyResource) Expect(err).To(HaveOccurred()) By("Creating the RRset resource") - resource := &dnsv1alpha2.RRset{ + resource := &dnsv1alpha3.RRset{ ObjectMeta: metav1.ObjectMeta{ Name: resourceName, Namespace: resourceNamespace, @@ -113,8 +115,8 @@ var _ = Describe("RRset Controller", func() { resource.SetResourceVersion("") comment := resourceComment _, err = controllerutil.CreateOrUpdate(ctx, k8sClient, resource, func() error { - resource.Spec = dnsv1alpha2.RRsetSpec{ - ZoneRef: dnsv1alpha2.ZoneRef{ + resource.Spec = dnsv1alpha3.RRsetSpec{ + ZoneRef: dnsv1alpha3.ZoneRef{ Name: zoneRef, Kind: resourceZoneKind, }, @@ -142,7 +144,7 @@ var _ = Describe("RRset Controller", func() { AfterEach(func() { ctx := context.Background() - resource := &dnsv1alpha2.RRset{} + resource := &dnsv1alpha3.RRset{} err := k8sClient.Get(ctx, rssetLookupKey, resource) Expect(err).NotTo(HaveOccurred()) @@ -156,7 +158,7 @@ var _ = Describe("RRset Controller", func() { }, timeout, interval).Should(BeTrue()) By("Cleaning up the specific resource instance Zone") - zone := &dnsv1alpha2.Zone{} + zone := &dnsv1alpha3.Zone{} err = k8sClient.Get(ctx, zoneLookupKey, zone) Expect(err).NotTo(HaveOccurred()) Expect(k8sClient.Delete(ctx, zone)).To(Succeed()) @@ -178,7 +180,7 @@ var _ = Describe("RRset Controller", func() { ic := countRrsetsMetrics() ctx := context.Background() By("Getting the existing resource") - createdResource := &dnsv1alpha2.RRset{} + createdResource := &dnsv1alpha3.RRset{} Eventually(func() bool { err := k8sClient.Get(ctx, rssetLookupKey, createdResource) return err == nil && createdResource.IsInExpectedStatus(FIRST_GENERATION, SUCCEEDED_STATUS) @@ -203,7 +205,7 @@ var _ = Describe("RRset Controller", func() { updatedRecords := []string{"127.0.0.3"} By("Getting the initial Serial of the zone") - zone := &dnsv1alpha2.Zone{} + zone := &dnsv1alpha3.Zone{} Eventually(func() bool { err := k8sClient.Get(ctx, zoneLookupKey, zone) return err == nil && zone.Status.Serial != nil @@ -211,7 +213,7 @@ var _ = Describe("RRset Controller", func() { initialSerial := zone.Status.Serial By("Updating RRset records") - resource := &dnsv1alpha2.RRset{ + resource := &dnsv1alpha3.RRset{ ObjectMeta: metav1.ObjectMeta{ Name: resourceName, Namespace: resourceNamespace, @@ -224,7 +226,7 @@ var _ = Describe("RRset Controller", func() { Expect(err).NotTo(HaveOccurred()) By("Getting the updated resource") - updatedRRset := &dnsv1alpha2.RRset{} + updatedRRset := &dnsv1alpha3.RRset{} Eventually(func() bool { err := k8sClient.Get(ctx, rssetLookupKey, updatedRRset) return err == nil && updatedRRset.IsInExpectedStatus(MODIFIED_GENERATION, SUCCEEDED_STATUS) @@ -236,7 +238,7 @@ var _ = Describe("RRset Controller", func() { Expect(getMockedComment(resourceName, resourceType)).To(Equal(resourceComment)) By("Getting the modified zone") - modifiedZone := &dnsv1alpha2.Zone{} + modifiedZone := &dnsv1alpha3.Zone{} Eventually(func() bool { err := k8sClient.Get(ctx, zoneLookupKey, modifiedZone) return err == nil && *modifiedZone.Status.Serial != *initialSerial @@ -255,7 +257,7 @@ var _ = Describe("RRset Controller", func() { modifiedResourceTTL := uint32(150) By("Getting the initial Serial of the zone") - zone := &dnsv1alpha2.Zone{} + zone := &dnsv1alpha3.Zone{} Eventually(func() bool { err := k8sClient.Get(ctx, zoneLookupKey, zone) _, found := readFromZonesMap(makeCanonical(zone.Name)) @@ -264,7 +266,7 @@ var _ = Describe("RRset Controller", func() { initialSerial := *zone.Status.Serial By("Updating RRset TTL") - resource := &dnsv1alpha2.RRset{ + resource := &dnsv1alpha3.RRset{ ObjectMeta: metav1.ObjectMeta{ Name: resourceName, Namespace: resourceNamespace, @@ -277,7 +279,7 @@ var _ = Describe("RRset Controller", func() { Expect(err).NotTo(HaveOccurred()) By("Getting the updated resource") - updatedRRset := &dnsv1alpha2.RRset{} + updatedRRset := &dnsv1alpha3.RRset{} Eventually(func() bool { err := k8sClient.Get(ctx, rssetLookupKey, updatedRRset) return err == nil && updatedRRset.IsInExpectedStatus(MODIFIED_GENERATION, SUCCEEDED_STATUS) @@ -289,7 +291,7 @@ var _ = Describe("RRset Controller", func() { Expect(getMockedComment(resourceName, resourceType)).To(Equal(resourceComment)) By("Getting the modified zone") - modifiedZone := &dnsv1alpha2.Zone{} + modifiedZone := &dnsv1alpha3.Zone{} Eventually(func() bool { err := k8sClient.Get(ctx, zoneLookupKey, modifiedZone) return err == nil && *modifiedZone.Status.Serial > initialSerial @@ -307,7 +309,7 @@ var _ = Describe("RRset Controller", func() { modifiedResourceComment := "Just another comment" By("Getting the initial Serial of the zone") - zone := &dnsv1alpha2.Zone{} + zone := &dnsv1alpha3.Zone{} Eventually(func() bool { err := k8sClient.Get(ctx, zoneLookupKey, zone) return err == nil && zone.Status.Serial != nil @@ -315,7 +317,7 @@ var _ = Describe("RRset Controller", func() { initialSerial := *zone.Status.Serial By("Updating RRset Comment") - resource := &dnsv1alpha2.RRset{ + resource := &dnsv1alpha3.RRset{ ObjectMeta: metav1.ObjectMeta{ Name: resourceName, Namespace: resourceNamespace, @@ -328,7 +330,7 @@ var _ = Describe("RRset Controller", func() { Expect(err).NotTo(HaveOccurred()) By("Getting the updated resource") - updatedRRset := &dnsv1alpha2.RRset{} + updatedRRset := &dnsv1alpha3.RRset{} Eventually(func() bool { err := k8sClient.Get(ctx, rssetLookupKey, updatedRRset) return err == nil && updatedRRset.IsInExpectedStatus(MODIFIED_GENERATION, SUCCEEDED_STATUS) @@ -340,7 +342,7 @@ var _ = Describe("RRset Controller", func() { Expect(getMockedComment(resourceName, resourceType)).To(Equal(modifiedResourceComment)) By("Getting the modified zone") - modifiedZone := &dnsv1alpha2.Zone{} + modifiedZone := &dnsv1alpha3.Zone{} Eventually(func() bool { err := k8sClient.Get(ctx, zoneLookupKey, modifiedZone) return err == nil && *modifiedZone.Status.Serial > initialSerial @@ -378,7 +380,7 @@ var _ = Describe("RRset Controller", func() { }) By("Recreating a RRset") - resource := &dnsv1alpha2.RRset{ + resource := &dnsv1alpha3.RRset{ ObjectMeta: metav1.ObjectMeta{ Name: recreationResourceName, Namespace: recreationResourceNamespace, @@ -386,13 +388,13 @@ var _ = Describe("RRset Controller", func() { } resource.SetResourceVersion("") _, err := controllerutil.CreateOrUpdate(ctx, k8sClient, resource, func() error { - resource.Spec = dnsv1alpha2.RRsetSpec{ + resource.Spec = dnsv1alpha3.RRsetSpec{ Type: recreationResourceType, TTL: recreationResourceTTL, Name: recreationResourceDNSName, Records: []string{recreationRecord}, Comment: &recreationResourceComment, - ZoneRef: dnsv1alpha2.ZoneRef{ + ZoneRef: dnsv1alpha3.ZoneRef{ Name: recreationZoneRef, Kind: resourceZoneKind, }, @@ -402,7 +404,7 @@ var _ = Describe("RRset Controller", func() { Expect(err).NotTo(HaveOccurred()) By("Getting the resource") - updatedRRset := &dnsv1alpha2.RRset{} + updatedRRset := &dnsv1alpha3.RRset{} typeNamespacedName := types.NamespacedName{ Name: recreationResourceName, Namespace: recreationResourceNamespace, @@ -456,7 +458,7 @@ var _ = Describe("RRset Controller", func() { }, timeout, interval).Should(BeTrue()) By("Modifying the deleted RRset") - resource := &dnsv1alpha2.RRset{ + resource := &dnsv1alpha3.RRset{ ObjectMeta: metav1.ObjectMeta{ Name: resourceName, Namespace: resourceNamespace, @@ -472,7 +474,7 @@ var _ = Describe("RRset Controller", func() { Expect(err).NotTo(HaveOccurred()) By("Getting the resource") - updatedRRset := &dnsv1alpha2.RRset{} + updatedRRset := &dnsv1alpha3.RRset{} typeNamespacedName := types.NamespacedName{ Name: resourceName, Namespace: resourceNamespace, @@ -506,7 +508,7 @@ var _ = Describe("RRset Controller", func() { fakeZoneRef := zoneName fakeRecords := []string{"127.0.0.11", "127.0.0.12"} - fakeResource := &dnsv1alpha2.RRset{ + fakeResource := &dnsv1alpha3.RRset{ ObjectMeta: metav1.ObjectMeta{ Name: fakeResourceName, Namespace: fakeResourceNamespace, @@ -514,13 +516,13 @@ var _ = Describe("RRset Controller", func() { } fakeResource.SetResourceVersion("") _, err := controllerutil.CreateOrUpdate(ctx, k8sClient, fakeResource, func() error { - fakeResource.Spec = dnsv1alpha2.RRsetSpec{ + fakeResource.Spec = dnsv1alpha3.RRsetSpec{ Type: fakeResourceType, Name: fakeResourceDNSName, TTL: fakeResourceTTL, Records: fakeRecords, Comment: &fakeResourceComment, - ZoneRef: dnsv1alpha2.ZoneRef{ + ZoneRef: dnsv1alpha3.ZoneRef{ Name: fakeZoneRef, Kind: resourceZoneKind, }, @@ -567,7 +569,7 @@ var _ = Describe("RRset Controller", func() { additionalResourceComment := "This is a AAAA Record" By("Creating the RRset resource") - additionalResource := &dnsv1alpha2.RRset{ + additionalResource := &dnsv1alpha3.RRset{ ObjectMeta: metav1.ObjectMeta{ Name: additionalResourceName, Namespace: resourceNamespace, @@ -575,8 +577,8 @@ var _ = Describe("RRset Controller", func() { } additionalResource.SetResourceVersion("") _, err := controllerutil.CreateOrUpdate(ctx, k8sClient, additionalResource, func() error { - additionalResource.Spec = dnsv1alpha2.RRsetSpec{ - ZoneRef: dnsv1alpha2.ZoneRef{ + additionalResource.Spec = dnsv1alpha3.RRsetSpec{ + ZoneRef: dnsv1alpha3.ZoneRef{ Name: zoneRef, Kind: resourceZoneKind, }, @@ -606,7 +608,7 @@ var _ = Describe("RRset Controller", func() { }, timeout, interval).Should(BeTrue()) By("Getting the created resource") - createdResource := &dnsv1alpha2.RRset{} + createdResource := &dnsv1alpha3.RRset{} Eventually(func() bool { err := k8sClient.Get(ctx, additionalRRsetLookupKey, createdResource) return err == nil && createdResource.IsInExpectedStatus(FIRST_GENERATION, SUCCEEDED_STATUS) @@ -635,7 +637,7 @@ var _ = Describe("RRset Controller", func() { additionalResourceComment := "This is a CNAME Record" By("Creating the RRset resource") - additionalResource := &dnsv1alpha2.RRset{ + additionalResource := &dnsv1alpha3.RRset{ ObjectMeta: metav1.ObjectMeta{ Name: additionalResourceName, Namespace: resourceNamespace, @@ -643,8 +645,8 @@ var _ = Describe("RRset Controller", func() { } additionalResource.SetResourceVersion("") _, err := controllerutil.CreateOrUpdate(ctx, k8sClient, additionalResource, func() error { - additionalResource.Spec = dnsv1alpha2.RRsetSpec{ - ZoneRef: dnsv1alpha2.ZoneRef{ + additionalResource.Spec = dnsv1alpha3.RRsetSpec{ + ZoneRef: dnsv1alpha3.ZoneRef{ Name: zoneRef, Kind: resourceZoneKind, }, @@ -674,7 +676,7 @@ var _ = Describe("RRset Controller", func() { }, timeout, interval).Should(BeTrue()) By("Getting the created resource") - createdResource := &dnsv1alpha2.RRset{} + createdResource := &dnsv1alpha3.RRset{} Eventually(func() bool { err := k8sClient.Get(ctx, additionalRRsetLookupKey, createdResource) return err == nil && createdResource.IsInExpectedStatus(FIRST_GENERATION, SUCCEEDED_STATUS) @@ -702,7 +704,7 @@ var _ = Describe("RRset Controller", func() { additionalResourceComment := "This is a A Wildcard Record" By("Creating the RRset resource") - additionalResource := &dnsv1alpha2.RRset{ + additionalResource := &dnsv1alpha3.RRset{ ObjectMeta: metav1.ObjectMeta{ Name: additionalResourceName, Namespace: resourceNamespace, @@ -710,8 +712,8 @@ var _ = Describe("RRset Controller", func() { } additionalResource.SetResourceVersion("") _, err := controllerutil.CreateOrUpdate(ctx, k8sClient, additionalResource, func() error { - additionalResource.Spec = dnsv1alpha2.RRsetSpec{ - ZoneRef: dnsv1alpha2.ZoneRef{ + additionalResource.Spec = dnsv1alpha3.RRsetSpec{ + ZoneRef: dnsv1alpha3.ZoneRef{ Name: zoneRef, Kind: resourceZoneKind, }, @@ -741,7 +743,7 @@ var _ = Describe("RRset Controller", func() { }, timeout, interval).Should(BeTrue()) By("Getting the created resource") - createdResource := &dnsv1alpha2.RRset{} + createdResource := &dnsv1alpha3.RRset{} Eventually(func() bool { err := k8sClient.Get(ctx, additionalRRsetLookupKey, createdResource) return err == nil && createdResource.IsInExpectedStatus(FIRST_GENERATION, SUCCEEDED_STATUS) @@ -768,7 +770,7 @@ var _ = Describe("RRset Controller", func() { additionalResourceComment := "This is a MX Record" By("Creating the RRset resource") - additionalResource := &dnsv1alpha2.RRset{ + additionalResource := &dnsv1alpha3.RRset{ ObjectMeta: metav1.ObjectMeta{ Name: additionalResourceName, Namespace: resourceNamespace, @@ -776,8 +778,8 @@ var _ = Describe("RRset Controller", func() { } additionalResource.SetResourceVersion("") _, err := controllerutil.CreateOrUpdate(ctx, k8sClient, additionalResource, func() error { - additionalResource.Spec = dnsv1alpha2.RRsetSpec{ - ZoneRef: dnsv1alpha2.ZoneRef{ + additionalResource.Spec = dnsv1alpha3.RRsetSpec{ + ZoneRef: dnsv1alpha3.ZoneRef{ Name: zoneRef, Kind: resourceZoneKind, }, @@ -807,7 +809,7 @@ var _ = Describe("RRset Controller", func() { }, timeout, interval).Should(BeTrue()) By("Getting the created resource") - createdResource := &dnsv1alpha2.RRset{} + createdResource := &dnsv1alpha3.RRset{} Eventually(func() bool { err := k8sClient.Get(ctx, additionalRRsetLookupKey, createdResource) return err == nil && createdResource.IsInExpectedStatus(FIRST_GENERATION, SUCCEEDED_STATUS) @@ -835,7 +837,7 @@ var _ = Describe("RRset Controller", func() { additionalResourceComment := "This is a NS Record" By("Creating the RRset resource") - additionalResource := &dnsv1alpha2.RRset{ + additionalResource := &dnsv1alpha3.RRset{ ObjectMeta: metav1.ObjectMeta{ Name: additionalResourceName, Namespace: resourceNamespace, @@ -843,8 +845,8 @@ var _ = Describe("RRset Controller", func() { } additionalResource.SetResourceVersion("") _, err := controllerutil.CreateOrUpdate(ctx, k8sClient, additionalResource, func() error { - additionalResource.Spec = dnsv1alpha2.RRsetSpec{ - ZoneRef: dnsv1alpha2.ZoneRef{ + additionalResource.Spec = dnsv1alpha3.RRsetSpec{ + ZoneRef: dnsv1alpha3.ZoneRef{ Name: zoneRef, Kind: resourceZoneKind, }, @@ -874,7 +876,7 @@ var _ = Describe("RRset Controller", func() { }, timeout, interval).Should(BeTrue()) By("Getting the created resource") - createdResource := &dnsv1alpha2.RRset{} + createdResource := &dnsv1alpha3.RRset{} Eventually(func() bool { err := k8sClient.Get(ctx, additionalRRsetLookupKey, createdResource) return err == nil && createdResource.IsInExpectedStatus(FIRST_GENERATION, SUCCEEDED_STATUS) @@ -902,7 +904,7 @@ var _ = Describe("RRset Controller", func() { additionalResourceComment := "This is a TXT Record" By("Creating the RRset resource") - additionalResource := &dnsv1alpha2.RRset{ + additionalResource := &dnsv1alpha3.RRset{ ObjectMeta: metav1.ObjectMeta{ Name: additionalResourceName, Namespace: resourceNamespace, @@ -910,8 +912,8 @@ var _ = Describe("RRset Controller", func() { } additionalResource.SetResourceVersion("") _, err := controllerutil.CreateOrUpdate(ctx, k8sClient, additionalResource, func() error { - additionalResource.Spec = dnsv1alpha2.RRsetSpec{ - ZoneRef: dnsv1alpha2.ZoneRef{ + additionalResource.Spec = dnsv1alpha3.RRsetSpec{ + ZoneRef: dnsv1alpha3.ZoneRef{ Name: zoneRef, Kind: resourceZoneKind, }, @@ -941,7 +943,7 @@ var _ = Describe("RRset Controller", func() { }, timeout, interval).Should(BeTrue()) By("Getting the created resource") - createdResource := &dnsv1alpha2.RRset{} + createdResource := &dnsv1alpha3.RRset{} Eventually(func() bool { err := k8sClient.Get(ctx, additionalRRsetLookupKey, createdResource) return err == nil && createdResource.IsInExpectedStatus(FIRST_GENERATION, SUCCEEDED_STATUS) @@ -969,7 +971,7 @@ var _ = Describe("RRset Controller", func() { additionalResourceComment := "This is a SRV Record" By("Creating the RRset resource") - additionalResource := &dnsv1alpha2.RRset{ + additionalResource := &dnsv1alpha3.RRset{ ObjectMeta: metav1.ObjectMeta{ Name: additionalResourceName, Namespace: resourceNamespace, @@ -977,8 +979,8 @@ var _ = Describe("RRset Controller", func() { } additionalResource.SetResourceVersion("") _, err := controllerutil.CreateOrUpdate(ctx, k8sClient, additionalResource, func() error { - additionalResource.Spec = dnsv1alpha2.RRsetSpec{ - ZoneRef: dnsv1alpha2.ZoneRef{ + additionalResource.Spec = dnsv1alpha3.RRsetSpec{ + ZoneRef: dnsv1alpha3.ZoneRef{ Name: zoneRef, Kind: resourceZoneKind, }, @@ -1008,7 +1010,7 @@ var _ = Describe("RRset Controller", func() { }, timeout, interval).Should(BeTrue()) By("Getting the created resource") - createdResource := &dnsv1alpha2.RRset{} + createdResource := &dnsv1alpha3.RRset{} Eventually(func() bool { err := k8sClient.Get(ctx, additionalRRsetLookupKey, createdResource) return err == nil && createdResource.IsInExpectedStatus(FIRST_GENERATION, SUCCEEDED_STATUS) @@ -1039,7 +1041,7 @@ var _ = Describe("RRset Controller", func() { additionalResourceComment := "This is a PTR Record" By("Creating the Reverse Zone resource") - reverseZone := &dnsv1alpha2.Zone{ + reverseZone := &dnsv1alpha3.Zone{ ObjectMeta: metav1.ObjectMeta{ Name: reverseZoneName, Namespace: reverseZoneNamespace, @@ -1047,7 +1049,8 @@ var _ = Describe("RRset Controller", func() { } reverseZone.SetResourceVersion("") _, err := controllerutil.CreateOrUpdate(ctx, k8sClient, reverseZone, func() error { - reverseZone.Spec = dnsv1alpha2.ZoneSpec{ + reverseZone.Spec = dnsv1alpha3.ZoneSpec{ + ProviderRef: zoneProviderRef, Kind: zoneKind, Nameservers: []string{zoneNS1, zoneNS2}, } @@ -1069,7 +1072,7 @@ var _ = Describe("RRset Controller", func() { }, timeout, interval).Should(BeTrue()) By("Creating the RRset resource") - additionalResource := &dnsv1alpha2.RRset{ + additionalResource := &dnsv1alpha3.RRset{ ObjectMeta: metav1.ObjectMeta{ Name: additionalResourceName, Namespace: additionalResourceNamespace, @@ -1077,8 +1080,8 @@ var _ = Describe("RRset Controller", func() { } additionalResource.SetResourceVersion("") _, err = controllerutil.CreateOrUpdate(ctx, k8sClient, additionalResource, func() error { - additionalResource.Spec = dnsv1alpha2.RRsetSpec{ - ZoneRef: dnsv1alpha2.ZoneRef{ + additionalResource.Spec = dnsv1alpha3.RRsetSpec{ + ZoneRef: dnsv1alpha3.ZoneRef{ Name: reverseZoneName, Kind: resourceZoneKind, }, @@ -1108,7 +1111,7 @@ var _ = Describe("RRset Controller", func() { }, timeout, interval).Should(BeTrue()) By("Getting the created resource") - createdResource := &dnsv1alpha2.RRset{} + createdResource := &dnsv1alpha3.RRset{} Eventually(func() bool { err := k8sClient.Get(ctx, additionalRRsetLookupKey, createdResource) return err == nil && createdResource.IsInExpectedStatus(FIRST_GENERATION, SUCCEEDED_STATUS) @@ -1137,7 +1140,7 @@ var _ = Describe("RRset Controller", func() { badTypeResourceComment := "This is a wrong-type Record" By("Creating the RRset resource") - badTypeResource := &dnsv1alpha2.RRset{ + badTypeResource := &dnsv1alpha3.RRset{ ObjectMeta: metav1.ObjectMeta{ Name: badTypeResourceName, Namespace: resourceNamespace, @@ -1145,8 +1148,8 @@ var _ = Describe("RRset Controller", func() { } badTypeResource.SetResourceVersion("") _, err := controllerutil.CreateOrUpdate(ctx, k8sClient, badTypeResource, func() error { - badTypeResource.Spec = dnsv1alpha2.RRsetSpec{ - ZoneRef: dnsv1alpha2.ZoneRef{ + badTypeResource.Spec = dnsv1alpha3.RRsetSpec{ + ZoneRef: dnsv1alpha3.ZoneRef{ Name: zoneName, Kind: resourceZoneKind, }, @@ -1172,7 +1175,7 @@ var _ = Describe("RRset Controller", func() { DnsFqdn := getRRsetName(badTypeResource) By("Getting the created resource") - createdResource := &dnsv1alpha2.RRset{} + createdResource := &dnsv1alpha3.RRset{} Eventually(func() bool { err := k8sClient.Get(ctx, badTypeRRsetLookupKey, createdResource) return err == nil && createdResource.IsInExpectedStatus(FIRST_GENERATION, FAILED_STATUS) @@ -1200,7 +1203,7 @@ var _ = Describe("RRset Controller", func() { badFormatResourceComment := "This is a wrong-format Record" By("Creating the RRset resource") - badFormatResource := &dnsv1alpha2.RRset{ + badFormatResource := &dnsv1alpha3.RRset{ ObjectMeta: metav1.ObjectMeta{ Name: badFormatResourceName, Namespace: resourceNamespace, @@ -1208,8 +1211,8 @@ var _ = Describe("RRset Controller", func() { } badFormatResource.SetResourceVersion("") _, err := controllerutil.CreateOrUpdate(ctx, k8sClient, badFormatResource, func() error { - badFormatResource.Spec = dnsv1alpha2.RRsetSpec{ - ZoneRef: dnsv1alpha2.ZoneRef{ + badFormatResource.Spec = dnsv1alpha3.RRsetSpec{ + ZoneRef: dnsv1alpha3.ZoneRef{ Name: zoneName, Kind: resourceZoneKind, }, @@ -1235,7 +1238,7 @@ var _ = Describe("RRset Controller", func() { DnsFqdn := getRRsetName(badFormatResource) By("Getting the created resource") - createdResource := &dnsv1alpha2.RRset{} + createdResource := &dnsv1alpha3.RRset{} Eventually(func() bool { err := k8sClient.Get(ctx, badFormatRRsetLookupKey, createdResource) return err == nil && createdResource.IsInExpectedStatus(FIRST_GENERATION, FAILED_STATUS) @@ -1263,7 +1266,7 @@ var _ = Describe("RRset Controller", func() { unquotedResourceComment := "This is an unquoted-TXT Record" By("Creating the RRset resource") - unquotedResource := &dnsv1alpha2.RRset{ + unquotedResource := &dnsv1alpha3.RRset{ ObjectMeta: metav1.ObjectMeta{ Name: unquotedResourceName, Namespace: resourceNamespace, @@ -1271,8 +1274,8 @@ var _ = Describe("RRset Controller", func() { } unquotedResource.SetResourceVersion("") _, err := controllerutil.CreateOrUpdate(ctx, k8sClient, unquotedResource, func() error { - unquotedResource.Spec = dnsv1alpha2.RRsetSpec{ - ZoneRef: dnsv1alpha2.ZoneRef{ + unquotedResource.Spec = dnsv1alpha3.RRsetSpec{ + ZoneRef: dnsv1alpha3.ZoneRef{ Name: zoneName, Kind: resourceZoneKind, }, @@ -1298,7 +1301,7 @@ var _ = Describe("RRset Controller", func() { DnsFqdn := getRRsetName(unquotedResource) By("Getting the created resource") - createdResource := &dnsv1alpha2.RRset{} + createdResource := &dnsv1alpha3.RRset{} Eventually(func() bool { err := k8sClient.Get(ctx, unquotedRRsetLookupKey, createdResource) return err == nil && createdResource.IsInExpectedStatus(FIRST_GENERATION, FAILED_STATUS) @@ -1328,7 +1331,7 @@ var _ = Describe("RRset Controller", func() { existingResourceTTL := uint32(300) By("Creating the RRset resource") - existingResource := &dnsv1alpha2.RRset{ + existingResource := &dnsv1alpha3.RRset{ ObjectMeta: metav1.ObjectMeta{ Name: existingResourceName, Namespace: existingResourceNamespace, @@ -1336,8 +1339,8 @@ var _ = Describe("RRset Controller", func() { } existingResource.SetResourceVersion("") _, err := controllerutil.CreateOrUpdate(ctx, k8sClient, existingResource, func() error { - existingResource.Spec = dnsv1alpha2.RRsetSpec{ - ZoneRef: dnsv1alpha2.ZoneRef{ + existingResource.Spec = dnsv1alpha3.RRsetSpec{ + ZoneRef: dnsv1alpha3.ZoneRef{ Name: zoneName, Kind: resourceZoneKind, }, @@ -1361,7 +1364,7 @@ var _ = Describe("RRset Controller", func() { }, timeout, interval).Should(BeTrue()) By("Getting the created resource") - createdResource := &dnsv1alpha2.RRset{} + createdResource := &dnsv1alpha3.RRset{} Eventually(func() bool { err := k8sClient.Get(ctx, existingRRsetLookupKey, createdResource) return err == nil && createdResource.IsInExpectedStatus(FIRST_GENERATION, FAILED_STATUS) @@ -1390,7 +1393,7 @@ var _ = Describe("RRset Controller", func() { pendingResourceTTL := uint32(300) By("Creating the RRset resource") - pendingResource := &dnsv1alpha2.RRset{ + pendingResource := &dnsv1alpha3.RRset{ ObjectMeta: metav1.ObjectMeta{ Name: pendingResourceName, Namespace: pendingResourceNamespace, @@ -1398,8 +1401,8 @@ var _ = Describe("RRset Controller", func() { } pendingResource.SetResourceVersion("") _, err := controllerutil.CreateOrUpdate(ctx, k8sClient, pendingResource, func() error { - pendingResource.Spec = dnsv1alpha2.RRsetSpec{ - ZoneRef: dnsv1alpha2.ZoneRef{ + pendingResource.Spec = dnsv1alpha3.RRsetSpec{ + ZoneRef: dnsv1alpha3.ZoneRef{ Name: pendingZoneName, Kind: resourceZoneKind, }, @@ -1423,7 +1426,7 @@ var _ = Describe("RRset Controller", func() { }, timeout, interval).Should(BeTrue()) By("Getting the created resource") - createdResource := &dnsv1alpha2.RRset{} + createdResource := &dnsv1alpha3.RRset{} Eventually(func() bool { err := k8sClient.Get(ctx, pendingRRsetLookupKey, createdResource) return err == nil && createdResource.IsInExpectedStatus(FIRST_GENERATION, PENDING_STATUS) @@ -1454,14 +1457,15 @@ var _ = Describe("RRset Controller", func() { recreationResourceRecords := []string{"1.2.3.4", "5.6.7.8"} By("Creating a ClusterZone") - recreationZone := &dnsv1alpha2.ClusterZone{ + recreationZone := &dnsv1alpha3.ClusterZone{ ObjectMeta: metav1.ObjectMeta{ Name: recreationZoneName, }, } recreationZone.SetResourceVersion("") _, err := controllerutil.CreateOrUpdate(ctx, k8sClient, recreationZone, func() error { - recreationZone.Spec = dnsv1alpha2.ZoneSpec{ + recreationZone.Spec = dnsv1alpha3.ZoneSpec{ + ProviderRef: zoneProviderRef, Kind: recreationZoneKind, Nameservers: recreationZoneNameservers, } @@ -1482,7 +1486,7 @@ var _ = Describe("RRset Controller", func() { }, timeout, interval).Should(BeTrue()) By("Creating a RRset") - recreationResource := &dnsv1alpha2.RRset{ + recreationResource := &dnsv1alpha3.RRset{ ObjectMeta: metav1.ObjectMeta{ Name: recreationResourceName, Namespace: recreationResourceNamespace, @@ -1490,8 +1494,8 @@ var _ = Describe("RRset Controller", func() { } recreationResource.SetResourceVersion("") _, err = controllerutil.CreateOrUpdate(ctx, k8sClient, recreationResource, func() error { - recreationResource.Spec = dnsv1alpha2.RRsetSpec{ - ZoneRef: dnsv1alpha2.ZoneRef{ + recreationResource.Spec = dnsv1alpha3.RRsetSpec{ + ZoneRef: dnsv1alpha3.ZoneRef{ Name: recreationZoneName, Kind: recreationResourceZoneKind, }, @@ -1520,20 +1524,20 @@ var _ = Describe("RRset Controller", func() { }, timeout, interval).Should(BeTrue()) By("Creating a ClusterRRset") - resource := &dnsv1alpha2.ClusterRRset{ + resource := &dnsv1alpha3.ClusterRRset{ ObjectMeta: metav1.ObjectMeta{ Name: recreationResourceName, }, } resource.SetResourceVersion("") _, err = controllerutil.CreateOrUpdate(ctx, k8sClient, resource, func() error { - resource.Spec = dnsv1alpha2.RRsetSpec{ + resource.Spec = dnsv1alpha3.RRsetSpec{ Type: recreationResourceType, Name: recreationResourceDNSName, TTL: recreationResourceTTL, Records: recreationResourceRecords, Comment: &recreationResourceComment, - ZoneRef: dnsv1alpha2.ZoneRef{ + ZoneRef: dnsv1alpha3.ZoneRef{ Name: recreationZoneName, Kind: recreationResourceZoneKind, }, @@ -1543,7 +1547,7 @@ var _ = Describe("RRset Controller", func() { Expect(err).NotTo(HaveOccurred()) By("Getting the resource") - recreatedZone := &dnsv1alpha2.ClusterRRset{} + recreatedZone := &dnsv1alpha3.ClusterRRset{} typeNamespacedName := types.NamespacedName{ Name: recreationResourceName, } diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go index 7b8e4d7..47ec656 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -41,13 +41,18 @@ import ( logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" - dnsv1alpha2 "github.com/powerdns-operator/powerdns-operator/api/v1alpha2" + dnsv1alpha3 "github.com/powerdns-operator/powerdns-operator/api/v1alpha3" //+kubebuilder:scaffold:imports ) // These tests use Ginkgo (BDD-style Go testing framework). Refer to // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. +const ( + // Test constants for consistent provider reference + testProviderRef = "test-powerdns" +) + var ( cfg *rest.Config k8sClient client.Client @@ -164,7 +169,7 @@ var _ = BeforeSuite(func() { // Note that you must have the required binaries setup under the bin directory to perform // the tests directly. When we run make test it will be setup and used automatically. BinaryAssetsDirectory: filepath.Join("..", "..", "bin", "k8s", - fmt.Sprintf("1.31.0-%s-%s", runtime.GOOS, runtime.GOARCH)), + fmt.Sprintf("1.33.0-%s-%s", runtime.GOOS, runtime.GOARCH)), } var err error @@ -173,7 +178,7 @@ var _ = BeforeSuite(func() { Expect(err).NotTo(HaveOccurred()) Expect(cfg).NotTo(BeNil()) - err = dnsv1alpha2.AddToScheme(scheme.Scheme) + err = dnsv1alpha3.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) //+kubebuilder:scaffold:scheme @@ -187,45 +192,42 @@ var _ = BeforeSuite(func() { }) Expect(err).ToNot(HaveOccurred()) - // Initialize mockClient - m := NewMockClient() + // Override newPDNSClientFunc to return the global test PDNSClient + newPDNSClientFunc = func(ctx context.Context, k8sClient client.Client, providerRef string) (PdnsClienter, error) { + // Validate that the PDNSProvider exists + pdnsprovider := &dnsv1alpha3.PDNSProvider{} + if err := k8sClient.Get(ctx, client.ObjectKey{Name: providerRef}, pdnsprovider); err != nil { + return PdnsClienter{}, fmt.Errorf("pdnsprovider '%s' not found: %w", providerRef, err) + } + return PDNSClient, nil + } err = (&RRsetReconciler{ Client: k8sManager.GetClient(), Scheme: k8sManager.GetScheme(), - PDNSClient: PdnsClienter{ - Records: m.Records, - Zones: m.Zones, - }, }).SetupWithManager(k8sManager) Expect(err).ToNot(HaveOccurred()) err = (&ClusterRRsetReconciler{ Client: k8sManager.GetClient(), Scheme: k8sManager.GetScheme(), - PDNSClient: PdnsClienter{ - Records: m.Records, - Zones: m.Zones, - }, }).SetupWithManager(k8sManager) Expect(err).ToNot(HaveOccurred()) err = (&ZoneReconciler{ Client: k8sManager.GetClient(), Scheme: k8sManager.GetScheme(), - PDNSClient: PdnsClienter{ - Records: m.Records, - Zones: m.Zones, - }, }).SetupWithManager(k8sManager) Expect(err).ToNot(HaveOccurred()) err = (&ClusterZoneReconciler{ Client: k8sManager.GetClient(), Scheme: k8sManager.GetScheme(), - PDNSClient: PdnsClienter{ - Records: m.Records, - Zones: m.Zones, - }, + }).SetupWithManager(k8sManager) + Expect(err).ToNot(HaveOccurred()) + + err = (&PDNSProviderReconciler{ + Client: k8sManager.GetClient(), + Scheme: k8sManager.GetScheme(), }).SetupWithManager(k8sManager) Expect(err).ToNot(HaveOccurred()) @@ -263,6 +265,47 @@ var _ = BeforeSuite(func() { Expect(err).Should(Succeed()) } + /* + ##################################################################################### + # Test PowerDNS PDNSProvider and Secret creation + ##################################################################################### + */ + By("creating test PowerDNS secret") + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-powerdns-secret", + Namespace: "default", + }, + Data: map[string][]byte{ + "apikey": []byte("test-api-key"), + }, + } + _, err = controllerutil.CreateOrUpdate(ctx, k8sClient, secret, func() error { + return nil + }) + Expect(err).Should(Succeed()) + + By("creating test PowerDNS pdnsprovider") + pdnsprovider := &dnsv1alpha3.PDNSProvider{ + ObjectMeta: metav1.ObjectMeta{ + Name: testProviderRef, + }, + Spec: dnsv1alpha3.PDNSProviderSpec{ + URL: "http://localhost:8081/api/v1", + Credentials: dnsv1alpha3.PDNSProviderCredentials{ + SecretRef: dnsv1alpha3.PDNSProviderSecretRef{ + Name: "test-powerdns-secret", + Namespace: ptr.To("default"), + Key: ptr.To("apikey"), + }, + }, + }, + } + _, err = controllerutil.CreateOrUpdate(ctx, k8sClient, pdnsprovider, func() error { + return nil + }) + Expect(err).Should(Succeed()) + }) var _ = AfterSuite(func() { diff --git a/internal/controller/zone_controller.go b/internal/controller/zone_controller.go index 9dc18b2..46bd8e9 100644 --- a/internal/controller/zone_controller.go +++ b/internal/controller/zone_controller.go @@ -22,7 +22,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/metrics" - dnsv1alpha2 "github.com/powerdns-operator/powerdns-operator/api/v1alpha2" + dnsv1alpha3 "github.com/powerdns-operator/powerdns-operator/api/v1alpha3" ) const ( @@ -48,8 +48,7 @@ const ( // ZoneReconciler reconciles a Zone object type ZoneReconciler struct { client.Client - Scheme *runtime.Scheme - PDNSClient PdnsClienter + Scheme *runtime.Scheme } func init() { @@ -66,7 +65,7 @@ func (r *ZoneReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl. log.Info("Reconcile Zone", "Zone.Name", req.Name) // Get Zone - zone := &dnsv1alpha2.Zone{} + zone := &dnsv1alpha3.Zone{} err := r.Get(ctx, req.NamespacedName, zone) if err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) @@ -102,25 +101,32 @@ func (r *ZoneReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl. } } - return zoneReconcile(ctx, zone, isModified, isDeleted, r.Client, r.PDNSClient, log) + // Get the appropriate PowerDNS client + pdnsClient, err := GetPDNSClient(ctx, r.Client, zone.GetProviderRef()) + if err != nil { + log.Error(err, "Failed to get PowerDNS client") + return ctrl.Result{}, err + } + + return zoneReconcile(ctx, zone, isModified, isDeleted, r.Client, pdnsClient, log) } // SetupWithManager sets up the controller with the Manager. func (r *ZoneReconciler) SetupWithManager(mgr ctrl.Manager) error { // We use indexer to ensure that only one Zone/ClusterZone exists for one DNS entry - if err := mgr.GetFieldIndexer().IndexField(context.Background(), &dnsv1alpha2.Zone{}, "Zone.Entry.Name", func(rawObj client.Object) []string { + if err := mgr.GetFieldIndexer().IndexField(context.Background(), &dnsv1alpha3.Zone{}, "Zone.Entry.Name", func(rawObj client.Object) []string { // grab the Zone object, extract its name... var ZoneName string - if rawObj.(*dnsv1alpha2.Zone).Status.SyncStatus == nil || *rawObj.(*dnsv1alpha2.Zone).Status.SyncStatus == SUCCEEDED_STATUS { - ZoneName = (rawObj.(*dnsv1alpha2.Zone)).Name + if rawObj.(*dnsv1alpha3.Zone).Status.SyncStatus == nil || *rawObj.(*dnsv1alpha3.Zone).Status.SyncStatus == SUCCEEDED_STATUS { + ZoneName = (rawObj.(*dnsv1alpha3.Zone)).Name } return []string{ZoneName} }); err != nil { return err } return ctrl.NewControllerManagedBy(mgr). - For(&dnsv1alpha2.Zone{}). - Owns(&dnsv1alpha2.ClusterRRset{}). - Owns(&dnsv1alpha2.RRset{}). + For(&dnsv1alpha3.Zone{}). + Owns(&dnsv1alpha3.ClusterRRset{}). + Owns(&dnsv1alpha3.RRset{}). Complete(r) } diff --git a/internal/controller/zone_controller_test.go b/internal/controller/zone_controller_test.go index bd08924..6cdc171 100644 --- a/internal/controller/zone_controller_test.go +++ b/internal/controller/zone_controller_test.go @@ -26,16 +26,17 @@ import ( "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - dnsv1alpha2 "github.com/powerdns-operator/powerdns-operator/api/v1alpha2" + dnsv1alpha3 "github.com/powerdns-operator/powerdns-operator/api/v1alpha3" ) var _ = Describe("Zone Controller", func() { const ( - resourceName = "example1.org" - resourceNamespace = "example1" - resourceKind = NATIVE_KIND_ZONE - resourceCatalog = "catalog.example1.org." + resourceName = "example1.org" + resourceNamespace = "example1" + resourceKind = NATIVE_KIND_ZONE + resourceCatalog = "catalog.example1.org." + resourceProviderRef = "test-powerdns" timeout = time.Second * 5 interval = time.Millisecond * 250 @@ -50,7 +51,7 @@ var _ = Describe("Zone Controller", func() { BeforeEach(func() { ctx := context.Background() By("creating the Zone resource") - resource := &dnsv1alpha2.Zone{ + resource := &dnsv1alpha3.Zone{ ObjectMeta: metav1.ObjectMeta{ Name: resourceName, Namespace: resourceNamespace, @@ -58,7 +59,8 @@ var _ = Describe("Zone Controller", func() { } resource.SetResourceVersion("") _, err := controllerutil.CreateOrUpdate(ctx, k8sClient, resource, func() error { - resource.Spec = dnsv1alpha2.ZoneSpec{ + resource.Spec = dnsv1alpha3.ZoneSpec{ + ProviderRef: resourceProviderRef, Kind: resourceKind, Nameservers: resourceNameservers, Catalog: ptr.To(resourceCatalog), @@ -82,7 +84,7 @@ var _ = Describe("Zone Controller", func() { AfterEach(func() { ctx := context.Background() - resource := &dnsv1alpha2.Zone{} + resource := &dnsv1alpha3.Zone{} err := k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -107,7 +109,7 @@ var _ = Describe("Zone Controller", func() { ic := countZonesMetrics() ctx := context.Background() By("Getting the existing resource") - zone := &dnsv1alpha2.Zone{} + zone := &dnsv1alpha3.Zone{} Eventually(func() bool { err := k8sClient.Get(ctx, typeNamespacedName, zone) return err == nil && zone.IsInExpectedStatus(FIRST_GENERATION, SUCCEEDED_STATUS) @@ -129,7 +131,7 @@ var _ = Describe("Zone Controller", func() { modifiedResourceNameservers := []string{"ns1.example1.org", "ns2.example1.org", "ns3.example1.org"} By("Getting the initial Serial of the resource") - zone := &dnsv1alpha2.Zone{} + zone := &dnsv1alpha3.Zone{} Eventually(func() bool { err := k8sClient.Get(ctx, typeNamespacedName, zone) return err == nil && zone.Status.Serial != nil @@ -137,7 +139,7 @@ var _ = Describe("Zone Controller", func() { initialSerial := *zone.Status.Serial By("Modifying the resource") - resource := &dnsv1alpha2.Zone{ + resource := &dnsv1alpha3.Zone{ ObjectMeta: metav1.ObjectMeta{ Name: resourceName, Namespace: resourceNamespace, @@ -150,7 +152,7 @@ var _ = Describe("Zone Controller", func() { Expect(err).NotTo(HaveOccurred()) By("Getting the modified resource") - modifiedZone := &dnsv1alpha2.Zone{} + modifiedZone := &dnsv1alpha3.Zone{} // Waiting for the resource to be fully modified Eventually(func() bool { err := k8sClient.Get(ctx, typeNamespacedName, modifiedZone) @@ -169,7 +171,7 @@ var _ = Describe("Zone Controller", func() { var modifiedResourceKind = []string{MASTER_KIND_ZONE, NATIVE_KIND_ZONE, SLAVE_KIND_ZONE, PRODUCER_KIND_ZONE, CONSUMER_KIND_ZONE} By("Getting the initial Serial of the resource") - zone := &dnsv1alpha2.Zone{} + zone := &dnsv1alpha3.Zone{} Eventually(func() bool { err := k8sClient.Get(ctx, typeNamespacedName, zone) return err == nil && zone.Status.Serial != nil @@ -177,7 +179,7 @@ var _ = Describe("Zone Controller", func() { initialSerial := zone.Status.Serial By("Modifying the resource") - resource := &dnsv1alpha2.Zone{ + resource := &dnsv1alpha3.Zone{ ObjectMeta: metav1.ObjectMeta{ Name: resourceName, Namespace: resourceNamespace, @@ -192,7 +194,7 @@ var _ = Describe("Zone Controller", func() { Expect(err).NotTo(HaveOccurred()) By("Getting the modified resource") - modifiedZone := &dnsv1alpha2.Zone{} + modifiedZone := &dnsv1alpha3.Zone{} // Waiting for the resource to be fully modified Eventually(func() bool { err := k8sClient.Get(ctx, typeNamespacedName, modifiedZone) @@ -218,7 +220,7 @@ var _ = Describe("Zone Controller", func() { var modifiedResourceCatalog = []string{"", "catalog.other-domain.org.", ""} By("Getting the initial Serial of the resource") - zone := &dnsv1alpha2.Zone{} + zone := &dnsv1alpha3.Zone{} Eventually(func() bool { err := k8sClient.Get(ctx, typeNamespacedName, zone) return err == nil && zone.Status.Serial != nil @@ -226,7 +228,7 @@ var _ = Describe("Zone Controller", func() { initialSerial := zone.Status.Serial By("Modifying the resource") - resource := &dnsv1alpha2.Zone{ + resource := &dnsv1alpha3.Zone{ ObjectMeta: metav1.ObjectMeta{ Name: resourceName, Namespace: resourceNamespace, @@ -242,7 +244,7 @@ var _ = Describe("Zone Controller", func() { Expect(err).NotTo(HaveOccurred()) By("Getting the modified resource") - modifiedZone := &dnsv1alpha2.Zone{} + modifiedZone := &dnsv1alpha3.Zone{} // Waiting for the resource to be fully modified Eventually(func() bool { err := k8sClient.Get(ctx, typeNamespacedName, modifiedZone) @@ -263,14 +265,14 @@ var _ = Describe("Zone Controller", func() { var modifiedResourceSOAEditAPI = "EPOCH" By("Getting the initial Serial of the resource") - zone := &dnsv1alpha2.Zone{} + zone := &dnsv1alpha3.Zone{} Eventually(func() bool { err := k8sClient.Get(ctx, typeNamespacedName, zone) return err == nil && zone.Status.Serial != nil }, timeout, interval).Should(BeTrue()) By("Modifying the resource") - resource := &dnsv1alpha2.Zone{ + resource := &dnsv1alpha3.Zone{ ObjectMeta: metav1.ObjectMeta{ Name: resourceName, Namespace: resourceNamespace, @@ -284,7 +286,7 @@ var _ = Describe("Zone Controller", func() { Expect(err).NotTo(HaveOccurred()) By("Getting the modified resource") - modifiedZone := &dnsv1alpha2.Zone{} + modifiedZone := &dnsv1alpha3.Zone{} // Waiting for the resource to be fully modified Eventually(func() bool { err := k8sClient.Get(ctx, typeNamespacedName, modifiedZone) @@ -315,7 +317,7 @@ var _ = Describe("Zone Controller", func() { }) By("Recreating a Zone") - resource := &dnsv1alpha2.Zone{ + resource := &dnsv1alpha3.Zone{ ObjectMeta: metav1.ObjectMeta{ Name: recreationResourceName, Namespace: resourceNamespace, @@ -323,7 +325,8 @@ var _ = Describe("Zone Controller", func() { } resource.SetResourceVersion("") _, err := controllerutil.CreateOrUpdate(ctx, k8sClient, resource, func() error { - resource.Spec = dnsv1alpha2.ZoneSpec{ + resource.Spec = dnsv1alpha3.ZoneSpec{ + ProviderRef: resourceProviderRef, Kind: recreationResourceKind, Nameservers: recreationResourceNameservers, } @@ -332,7 +335,7 @@ var _ = Describe("Zone Controller", func() { Expect(err).NotTo(HaveOccurred()) By("Getting the resource") - updatedZone := &dnsv1alpha2.Zone{} + updatedZone := &dnsv1alpha3.Zone{} typeNamespacedName := types.NamespacedName{ Name: recreationResourceName, Namespace: resourceNamespace, @@ -381,7 +384,7 @@ var _ = Describe("Zone Controller", func() { }, timeout, interval).Should(BeTrue()) By("Modifying the deleted Zone") - resource := &dnsv1alpha2.Zone{ + resource := &dnsv1alpha3.Zone{ ObjectMeta: metav1.ObjectMeta{ Name: resourceName, Namespace: resourceNamespace, @@ -395,7 +398,7 @@ var _ = Describe("Zone Controller", func() { Expect(err).NotTo(HaveOccurred()) By("Getting the resource") - updatedZone := &dnsv1alpha2.Zone{} + updatedZone := &dnsv1alpha3.Zone{} // Waiting for the resource to be fully modified Eventually(func() bool { err := k8sClient.Get(ctx, typeNamespacedName, updatedZone) @@ -415,7 +418,7 @@ var _ = Describe("Zone Controller", func() { fakeResourceName := "fake.org" fakeResourceKind := NATIVE_KIND_ZONE fakeResourceNameservers := []string{"ns1.fake.org", "ns2.fake.org"} - fakeResource := &dnsv1alpha2.Zone{ + fakeResource := &dnsv1alpha3.Zone{ ObjectMeta: metav1.ObjectMeta{ Name: fakeResourceName, Namespace: resourceNamespace, @@ -423,7 +426,8 @@ var _ = Describe("Zone Controller", func() { } fakeResource.SetResourceVersion("") _, err := controllerutil.CreateOrUpdate(ctx, k8sClient, fakeResource, func() error { - fakeResource.Spec = dnsv1alpha2.ZoneSpec{ + fakeResource.Spec = dnsv1alpha3.ZoneSpec{ + ProviderRef: resourceProviderRef, Kind: fakeResourceKind, Nameservers: fakeResourceNameservers, } @@ -467,7 +471,7 @@ var _ = Describe("Zone Controller", func() { recreationResourceNameservers := []string{"ns1.example1.org", "ns2.example1.org"} By("Creating a Zone") - resource := &dnsv1alpha2.Zone{ + resource := &dnsv1alpha3.Zone{ ObjectMeta: metav1.ObjectMeta{ Name: recreationResourceName, Namespace: recreationResourceNamespace, @@ -475,7 +479,8 @@ var _ = Describe("Zone Controller", func() { } resource.SetResourceVersion("") _, err := controllerutil.CreateOrUpdate(ctx, k8sClient, resource, func() error { - resource.Spec = dnsv1alpha2.ZoneSpec{ + resource.Spec = dnsv1alpha3.ZoneSpec{ + ProviderRef: resourceProviderRef, Kind: recreationResourceKind, Nameservers: recreationResourceNameservers, Catalog: ptr.To(recreationResourceCatalog), @@ -485,7 +490,7 @@ var _ = Describe("Zone Controller", func() { Expect(err).NotTo(HaveOccurred()) By("Getting the resource") - updatedZone := &dnsv1alpha2.Zone{} + updatedZone := &dnsv1alpha3.Zone{} typeNamespacedName := types.NamespacedName{ Name: recreationResourceName, Namespace: recreationResourceNamespace, @@ -507,24 +512,25 @@ var _ = Describe("Zone Controller", func() { recreationResourceNameservers := []string{"ns1.example1.org", "ns2.example1.org"} By("Creating a Zone") - resource := &dnsv1alpha2.ClusterZone{ + resource := &dnsv1alpha3.ClusterZone{ ObjectMeta: metav1.ObjectMeta{ Name: recreationResourceName, }, } resource.SetResourceVersion("") _, err := controllerutil.CreateOrUpdate(ctx, k8sClient, resource, func() error { - resource.Spec = dnsv1alpha2.ZoneSpec{ + resource.Spec = dnsv1alpha3.ZoneSpec{ Kind: recreationResourceKind, Nameservers: recreationResourceNameservers, Catalog: ptr.To(recreationResourceCatalog), + ProviderRef: resourceProviderRef, } return nil }) Expect(err).NotTo(HaveOccurred()) By("Getting the resource") - updatedZone := &dnsv1alpha2.ClusterZone{} + updatedZone := &dnsv1alpha3.ClusterZone{} typeNamespacedName := types.NamespacedName{ Name: recreationResourceName, }