diff --git a/api/v1alpha1/zone_types.go b/api/v1alpha1/zone_types.go index 04d10a8..5106c77 100644 --- a/api/v1alpha1/zone_types.go +++ b/api/v1alpha1/zone_types.go @@ -23,6 +23,9 @@ type ZoneSpec struct { // +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"` } // ZoneStatus defines the observed state of Zone diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index f048153..40c939c 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -204,6 +204,11 @@ func (in *ZoneSpec) DeepCopyInto(out *ZoneSpec) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.Catalog != nil { + in, out := &in.Catalog, &out.Catalog + *out = new(string) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ZoneSpec. diff --git a/config/crd/bases/dns.cav.enablers.ob_zones.yaml b/config/crd/bases/dns.cav.enablers.ob_zones.yaml index e1d9d9a..aa82282 100644 --- a/config/crd/bases/dns.cav.enablers.ob_zones.yaml +++ b/config/crd/bases/dns.cav.enablers.ob_zones.yaml @@ -46,6 +46,9 @@ spec: 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". diff --git a/go.mod b/go.mod index 93650d1..9d2c811 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/onsi/gomega v1.34.1 k8s.io/apimachinery v0.31.0 k8s.io/client-go v0.31.0 + k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 sigs.k8s.io/controller-runtime v0.19.0 ) @@ -67,7 +68,6 @@ require ( k8s.io/apiextensions-apiserver v0.31.0 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20240816214639-573285566f34 // indirect - k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect sigs.k8s.io/yaml v1.4.0 // indirect diff --git a/internal/controller/pdns_helper.go b/internal/controller/pdns_helper.go index da3fd66..38b2126 100644 --- a/internal/controller/pdns_helper.go +++ b/internal/controller/pdns_helper.go @@ -38,9 +38,17 @@ type PdnsClienter struct { Zones pdnsZonesClienter } -// zoneIsIdenticalToExternalZone return True, True if respectively kind and nameservers are identical between Zone and External Resource +// zoneIsIdenticalToExternalZone return True, True if respectively kind and Catalog are identical +// and nameservers are identical between Zone and External Resource func zoneIsIdenticalToExternalZone(zone *dnsv1alpha1.Zone, externalZone *powerdns.Zone, ns []string) (bool, bool) { - return zone.Spec.Kind == string(*externalZone.Kind), reflect.DeepEqual(zone.Spec.Nameservers, ns) + var zoneCatalog, externalZoneCatalog string + if zone.Spec.Catalog != nil { + zoneCatalog = *zone.Spec.Catalog + } + if externalZone.Catalog != nil { + externalZoneCatalog = *externalZone.Catalog + } + return zone.Spec.Kind == string(*externalZone.Kind) && zoneCatalog == externalZoneCatalog, reflect.DeepEqual(zone.Spec.Nameservers, ns) } // rrsetIsIdenticalToExternalRRset return True if Comments, Name, Type, TTL and Records are identical between RRSet and External Resource diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go index 0001719..210c528 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -258,7 +258,7 @@ func (m mockZonesClient) Change(ctx context.Context, domain string, zone *powerd return powerdns.Error{StatusCode: ZONE_NOT_FOUND_CODE, Status: fmt.Sprintf("%d %s", ZONE_NOT_FOUND_CODE, ZONE_NOT_FOUND_MSG), Message: ZONE_NOT_FOUND_MSG} } serial := localZone.Serial - if *zone.Kind != *localZone.Kind { + if *zone.Kind != *localZone.Kind || *zone.Catalog != *localZone.Catalog { serial = Uint32(*localZone.Serial + uint32(1)) } zone.Serial = serial @@ -389,3 +389,12 @@ func getMockedComment(rrsetName, rrsetType string) (result string) { } return } + +//nolint:unparam +func getMockedCatalog(zoneName string) (result string) { + zone, _ := readFromZonesMap(makeCanonical(zoneName)) + if zone.Kind != nil { + result = *zone.Catalog + } + return +} diff --git a/internal/controller/zone_controller.go b/internal/controller/zone_controller.go index a234e9b..09d9c10 100644 --- a/internal/controller/zone_controller.go +++ b/internal/controller/zone_controller.go @@ -202,10 +202,15 @@ func (r *ZoneReconciler) deleteExternalResources(ctx context.Context, zone *dnsv func (r *ZoneReconciler) updateExternalResources(ctx context.Context, zone *dnsv1alpha1.Zone) error { log := log.FromContext(ctx) zoneKind := powerdns.ZoneKind(zone.Spec.Kind) + catalog := "" + if zone.Spec.Catalog != nil { + catalog = *zone.Spec.Catalog + } err := r.PDNSClient.Zones.Change(ctx, zone.ObjectMeta.Name, &powerdns.Zone{ Name: &zone.ObjectMeta.Name, Kind: &zoneKind, Nameservers: zone.Spec.Nameservers, + Catalog: &catalog, }) if err != nil { log.Error(err, "Failed to update zone") @@ -246,6 +251,7 @@ func (r *ZoneReconciler) createExternalResources(ctx context.Context, zone *dnsv // SOAEditAPI: &soaEditApi, // APIRectify: &apiRectify, Nameservers: zone.Spec.Nameservers, + Catalog: zone.Spec.Catalog, } _, err := r.PDNSClient.Zones.Add(ctx, &z) diff --git a/internal/controller/zone_controller_test.go b/internal/controller/zone_controller_test.go index cadcb56..ab398e9 100644 --- a/internal/controller/zone_controller_test.go +++ b/internal/controller/zone_controller_test.go @@ -21,6 +21,7 @@ import ( "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" dnsv1alpha1 "github.com/orange-opensource/powerdns-operator/api/v1alpha1" @@ -29,8 +30,9 @@ import ( var _ = Describe("Zone Controller", func() { const ( - resourceName = "example1.org" - resourceKind = "Native" + resourceName = "example1.org" + resourceKind = "Native" + resourceCatalog = "catalog.example1.org." timeout = time.Second * 5 interval = time.Millisecond * 250 @@ -54,6 +56,7 @@ var _ = Describe("Zone Controller", func() { resource.Spec = dnsv1alpha1.ZoneSpec{ Kind: resourceKind, Nameservers: resourceNameservers, + Catalog: ptr.To(resourceCatalog), } return nil }) @@ -106,6 +109,7 @@ var _ = Describe("Zone Controller", func() { }, timeout, interval).Should(BeTrue()) Expect(getMockedKind(resourceName)).To(Equal(resourceKind), "Kind should be equal") Expect(getMockedNameservers(resourceName)).To(Equal(resourceNameservers), "Nameservers should be equal") + Expect(getMockedCatalog(resourceName)).To(Equal(resourceCatalog), "Catalog should be equal") Expect(zone.GetFinalizers()).To(ContainElement(FINALIZER_NAME), "Zone should contain the finalizer") Expect(fmt.Sprintf("%d", *(zone.Status.Serial))).To(Equal(fmt.Sprintf("%s01", time.Now().Format("20060102"))), "Serial should be YYYYMMDD01") }) @@ -193,6 +197,50 @@ var _ = Describe("Zone Controller", func() { }) }) + Context("When existing resource", func() { + It("should successfully modify the catalog of the zone", Label("zone-modification", "catalog"), func() { + ctx := context.Background() + // Specific test variables + var modifiedResourceCatalog = []string{"", "catalog.other-domain.org.", ""} + + By("Getting the initial Serial of the resource") + zone := &dnsv1alpha1.Zone{} + Eventually(func() bool { + err := k8sClient.Get(ctx, typeNamespacedName, zone) + return err == nil && zone.Status.Serial != nil + }, timeout, interval).Should(BeTrue()) + initialSerial := zone.Status.Serial + + By("Modifying the resource") + resource := &dnsv1alpha1.Zone{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + }, + } + // Update the resource for each kind and ensure the serial is incremented + for i, c := range modifiedResourceCatalog { + catalog := c + _, err := controllerutil.CreateOrUpdate(ctx, k8sClient, resource, func() error { + resource.Spec.Catalog = &catalog + return nil + }) + Expect(err).NotTo(HaveOccurred()) + + By("Getting the modified resource") + modifiedZone := &dnsv1alpha1.Zone{} + // Waiting for the resource to be fully modified + Eventually(func() bool { + err := k8sClient.Get(ctx, typeNamespacedName, modifiedZone) + return err == nil && *modifiedZone.Status.Serial > *initialSerial+uint32(i) + }, timeout, interval).Should(BeTrue()) + + expectedSerial := *initialSerial + uint32(i+1) + Expect(getMockedCatalog(resourceName)).To(Equal(catalog), "Catalog should be equal") + Expect(*(modifiedZone.Status.Serial)).To(Equal(expectedSerial), "Serial should be incremented") + } + }) + }) + Context("When existing resource", func() { It("should successfully recreate an existing zone", Label("zone-recreation"), func() { ctx := context.Background()