diff --git a/api/v1alpha1/zone_types.go b/api/v1alpha1/zone_types.go index 5106c77..4bf41db 100644 --- a/api/v1alpha1/zone_types.go +++ b/api/v1alpha1/zone_types.go @@ -26,6 +26,11 @@ type ZoneSpec struct { // 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 diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index e084b31..94eff61 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -224,6 +224,11 @@ func (in *ZoneSpec) DeepCopyInto(out *ZoneSpec) { *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. diff --git a/config/crd/bases/dns.cav.enablers.ob_zones.yaml b/config/crd/bases/dns.cav.enablers.ob_zones.yaml index 0aaf158..607dcef 100644 --- a/config/crd/bases/dns.cav.enablers.ob_zones.yaml +++ b/config/crd/bases/dns.cav.enablers.ob_zones.yaml @@ -66,6 +66,15 @@ spec: type: string minItems: 1 type: array + 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 diff --git a/config/samples/dns_v1alpha1_zone.yaml b/config/samples/dns_v1alpha1_zone.yaml index 4745800..ad33594 100644 --- a/config/samples/dns_v1alpha1_zone.yaml +++ b/config/samples/dns_v1alpha1_zone.yaml @@ -21,3 +21,30 @@ spec: - ns1.helloworld.com - ns2.helloworld.com kind: Native + +--- +# Specific Catalog +apiVersion: dns.cav.enablers.ob/v1alpha1 +kind: Zone +metadata: + name: example1.com +spec: + catalog: catalog.test + nameservers: + - ns1.example1.com + - ns2.example1.com + kind: Master + +--- +# Specific SOA_EDIT_API +apiVersion: dns.cav.enablers.ob/v1alpha1 +kind: Zone +metadata: + name: example2.com +spec: + catalog: catalog.test + nameservers: + - ns1.example2.com + - ns2.example2.com + kind: Master + soa_edit_api: EPOCH diff --git a/internal/controller/pdns_helper.go b/internal/controller/pdns_helper.go index 5b0b24a..049d813 100644 --- a/internal/controller/pdns_helper.go +++ b/internal/controller/pdns_helper.go @@ -39,12 +39,14 @@ type PdnsClienter struct { Zones pdnsZonesClienter } -// zoneIsIdenticalToExternalZone return True, True if respectively kind and Catalog are identical +// 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 *dnsv1alpha1.Zone, externalZone *powerdns.Zone, ns []string) (bool, bool) { zoneCatalog := makeCanonical(ptr.Deref(zone.Spec.Catalog, "")) externalZoneCatalog := ptr.Deref(externalZone.Catalog, "") - return zone.Spec.Kind == string(*externalZone.Kind) && zoneCatalog == externalZoneCatalog, reflect.DeepEqual(zone.Spec.Nameservers, ns) + zoneSOAEditAPI := ptr.Deref(zone.Spec.SOAEditAPI, "") + externalZoneSOAEditAPI := ptr.Deref(externalZone.SOAEditAPI, "") + return zone.Spec.Kind == string(*externalZone.Kind) && zoneCatalog == externalZoneCatalog && zoneSOAEditAPI == externalZoneSOAEditAPI, 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 7325fd8..3ef864b 100644 --- a/internal/controller/suite_test.go +++ b/internal/controller/suite_test.go @@ -16,6 +16,7 @@ import ( "fmt" "path/filepath" "reflect" + "regexp" "runtime" "slices" "strings" @@ -216,8 +217,16 @@ func (m mockZonesClient) Add(ctx context.Context, zone *powerdns.Zone) (*powerdn } // Serial initialization - now := time.Now().UTC() - serial := uint32(now.Year())*1000000 + uint32((now.Month()))*10000 + uint32(now.Day())*100 + 1 + var serial uint32 + switch *zone.SOAEditAPI { + case "EPOCH": + serial = uint32(time.Now().UTC().Unix()) + case "INCREASE": + serial = uint32(1) + default: + now := time.Now().UTC() + serial = uint32(now.Year())*1000000 + uint32((now.Month()))*10000 + uint32(now.Day())*100 + 1 + } zone.Serial = &serial // RRset type NS creation @@ -259,8 +268,21 @@ 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 || *zone.Catalog != *localZone.Catalog { - serial = ptr.To(*localZone.Serial + uint32(1)) + if *zone.Kind != *localZone.Kind || *zone.Catalog != *localZone.Catalog || *zone.SOAEditAPI != *localZone.SOAEditAPI { + switch *zone.SOAEditAPI { + case "EPOCH": + serial = ptr.To(uint32(time.Now().UTC().Unix())) + case "INCREASE": + serial = ptr.To(*localZone.Serial + uint32(1)) + default: + match, _ := regexp.MatchString("[0-9]{10}", fmt.Sprintf("%d", *localZone.Serial)) + if match { + serial = ptr.To(*localZone.Serial + uint32(1)) + break + } + now := time.Now().UTC() + serial = ptr.To(uint32(now.Year())*1000000 + uint32((now.Month()))*10000 + uint32(now.Day())*100 + 1) + } } zone.Serial = serial @@ -422,3 +444,9 @@ func getMockedCatalog(zoneName string) (result string) { result = ptr.Deref(zone.Catalog, "") return } + +func getMockedSOAEditAPI(zoneName string) (result string) { + zone, _ := readFromZonesMap(makeCanonical(zoneName)) + result = ptr.Deref(zone.SOAEditAPI, "") + return +} diff --git a/internal/controller/zone_controller.go b/internal/controller/zone_controller.go index be81dc5..3d639c9 100644 --- a/internal/controller/zone_controller.go +++ b/internal/controller/zone_controller.go @@ -106,7 +106,7 @@ func (r *ZoneReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl. return ctrl.Result{}, err } } else { - // If Zone exists, compare Type and Nameservers and update it if necessary + // If Zone exists, compare content and update it if necessary ns, err := r.PDNSClient.Records.Get(ctx, zone.ObjectMeta.Name, zone.ObjectMeta.Name, ptr.To(powerdns.RRTypeNS)) if err != nil { return ctrl.Result{}, err @@ -215,6 +215,7 @@ func (r *ZoneReconciler) updateExternalResources(ctx context.Context, zone *dnsv Kind: &zoneKind, Nameservers: zone.Spec.Nameservers, Catalog: catalog, + SOAEditAPI: zone.Spec.SOAEditAPI, }) if err != nil { log.Error(err, "Failed to update zone") @@ -253,13 +254,11 @@ func (r *ZoneReconciler) createExternalResources(ctx context.Context, zone *dnsv } z := powerdns.Zone{ - ID: &zone.Name, - Name: &zone.Name, - Kind: powerdns.ZoneKindPtr(powerdns.ZoneKind(zone.Spec.Kind)), - DNSsec: ptr.To(false), - // SOAEdit: &soaEdit, - // SOAEditAPI: &soaEditApi, - // APIRectify: &apiRectify, + ID: &zone.Name, + Name: &zone.Name, + Kind: powerdns.ZoneKindPtr(powerdns.ZoneKind(zone.Spec.Kind)), + DNSsec: ptr.To(false), + SOAEditAPI: zone.Spec.SOAEditAPI, Nameservers: zone.Spec.Nameservers, Catalog: catalog, } diff --git a/internal/controller/zone_controller_test.go b/internal/controller/zone_controller_test.go index 1fb8975..b674a3d 100644 --- a/internal/controller/zone_controller_test.go +++ b/internal/controller/zone_controller_test.go @@ -246,6 +246,45 @@ var _ = Describe("Zone Controller", func() { }) }) + Context("When existing resource", func() { + It("should successfully modify the catalog of the zone", Label("zone-modification", "soa-edit-api"), func() { + ctx := context.Background() + // Specific test variables + var modifiedResourceSOAEditAPI = "EPOCH" + + 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, + }, + } + epochSerial := uint32(time.Now().UTC().Unix()) + _, err := controllerutil.CreateOrUpdate(ctx, k8sClient, resource, func() error { + resource.Spec.SOAEditAPI = &modifiedResourceSOAEditAPI + 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 + }, timeout, interval).Should(BeTrue()) + Expect(getMockedSOAEditAPI(resourceName)).To(Equal(modifiedResourceSOAEditAPI), "SOA-Edit-API should have changed") + Expect(*(modifiedZone.Status.Serial)).To(Equal(epochSerial), "Serial should have changed") + }) + }) + Context("When existing resource", func() { It("should successfully recreate an existing zone", Label("zone-recreation"), func() { ctx := context.Background() @@ -259,9 +298,10 @@ var _ = Describe("Zone Controller", func() { now := time.Now().UTC() initialSerial := uint32(now.Year())*1000000 + uint32((now.Month()))*10000 + uint32(now.Day())*100 + 1 writeToZonesMap(makeCanonical(recreationResourceName), &powerdns.Zone{ - Name: &recreationResourceName, - Kind: powerdns.ZoneKindPtr(powerdns.ZoneKind(recreationResourceKind)), - Serial: &initialSerial, + Name: &recreationResourceName, + Kind: powerdns.ZoneKindPtr(powerdns.ZoneKind(recreationResourceKind)), + Serial: &initialSerial, + SOAEditAPI: ptr.To("DEFAULT"), }) By("Recreating a Zone")