diff --git a/api/v1alpha1/rrset_types.go b/api/v1alpha1/rrset_types.go index 954e5cc..f8bc7e4 100644 --- a/api/v1alpha1/rrset_types.go +++ b/api/v1alpha1/rrset_types.go @@ -18,6 +18,9 @@ import ( 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. @@ -36,15 +39,20 @@ type ZoneRef struct { // RRsetStatus defines the observed state of RRset type RRsetStatus struct { - LastUpdateTime *metav1.Time `json:"lastUpdateTime,omitempty"` + LastUpdateTime *metav1.Time `json:"lastUpdateTime,omitempty"` + DnsEntryName *string `json:"dnsEntryName,omitempty"` + SyncStatus *string `json:"syncStatus,omitempty"` + SyncErrorDescription *string `json:"syncErrorDescription,omitempty"` } // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +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 { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 40c939c..e084b31 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -110,6 +110,21 @@ func (in *RRsetStatus) DeepCopyInto(out *RRsetStatus) { 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.SyncErrorDescription != nil { + in, out := &in.SyncErrorDescription, &out.SyncErrorDescription + *out = new(string) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RRsetStatus. diff --git a/cmd/main.go b/cmd/main.go index 4ffce5d..f1593fb 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -82,7 +82,7 @@ func main() { 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") opts := zap.Options{ - Development: true, + Development: false, } opts.BindFlags(flag.CommandLine) flag.Parse() diff --git a/config/crd/bases/dns.cav.enablers.ob_rrsets.yaml b/config/crd/bases/dns.cav.enablers.ob_rrsets.yaml index 9b9ae2d..7283216 100644 --- a/config/crd/bases/dns.cav.enablers.ob_rrsets.yaml +++ b/config/crd/bases/dns.cav.enablers.ob_rrsets.yaml @@ -18,12 +18,18 @@ spec: - 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 @@ -55,6 +61,12 @@ spec: 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: @@ -77,6 +89,7 @@ spec: - name type: object required: + - name - records - ttl - type @@ -85,9 +98,15 @@ spec: status: description: RRsetStatus defines the observed state of RRset properties: + dnsEntryName: + type: string lastUpdateTime: format: date-time type: string + syncErrorDescription: + type: string + syncStatus: + type: string type: object type: object served: true diff --git a/config/samples/dns_v1alpha1_rrset.yaml b/config/samples/dns_v1alpha1_rrset.yaml index 3084cff..54175a8 100644 --- a/config/samples/dns_v1alpha1_rrset.yaml +++ b/config/samples/dns_v1alpha1_rrset.yaml @@ -1,12 +1,14 @@ +--- +# Record of type 'A' in 'helloworld.com' zone +# test.helloworld.com. IN A 1.1.1.1 +# test.helloworld.com. IN A 2.2.2.2 apiVersion: dns.cav.enablers.ob/v1alpha1 kind: RRset metadata: - labels: - app.kubernetes.io/name: powerdns-operator - app.kubernetes.io/managed-by: kustomize name: test.helloworld.com spec: comment: nothing to tell + name: test type: A ttl: 300 records: @@ -16,81 +18,212 @@ spec: name: helloworld.com --- +# FQDN Record of type 'A' in 'helloworld.com' zone +# test1.helloworld.com. IN A 3.3.3.3 apiVersion: dns.cav.enablers.ob/v1alpha1 kind: RRset metadata: - labels: - app.kubernetes.io/name: powerdns-operator - app.kubernetes.io/managed-by: kustomize name: test1.helloworld.com spec: + comment: nothing to tell + name: test1.helloworld.com. type: A ttl: 300 records: - - 1.1.1.1 + - 3.3.3.3 + zoneRef: + name: helloworld.com + +--- +# Record of type 'A' in 'helloworld.com' zone +# test2.helloworld.com.helloworld.com. IN A 3.3.3.3 +apiVersion: dns.cav.enablers.ob/v1alpha1 +kind: RRset +metadata: + name: test2.helloworld.com +spec: + type: A + name: test2.helloworld.com + ttl: 300 + records: + - 4.4.4.4 + zoneRef: + name: helloworld.com + +--- +# Record of type 'AAAA' in 'helloworld.com' zone +# test3.helloworld.com. IN AAAA 2001:0dc8:86a4:0000:0000:7a2f:2360:2341 +apiVersion: dns.cav.enablers.ob/v1alpha1 +kind: RRset +metadata: + name: test3-ipv6.helloworld.com +spec: + type: AAAA + name: test3 + ttl: 300 + records: + - 2001:0dc8:86a4:0000:0000:7a2f:2360:2341 + zoneRef: + name: helloworld.com + +--- +# Record of type 'CNAME' in 'helloworld.com' zone +# test4.helloworld.com. IN CNAME test1.helloworld.com. +apiVersion: dns.cav.enablers.ob/v1alpha1 +kind: RRset +metadata: + name: test4.helloworld.com +spec: + type: CNAME + name: test4 + ttl: 300 + records: + - test1.helloworld.com. zoneRef: name: helloworld.com -# --- -# apiVersion: dns.cav.enablers.ob/v1alpha1 -# kind: RRset -# metadata: -# labels: -# app.kubernetes.io/name: powerdns-operator -# app.kubernetes.io/managed-by: kustomize -# name: test2.helloworld.com -# spec: -# type: A -# ttl: 300 -# records: -# - 1.1.1.1 -# zoneRef: -# name: helloworld.com +--- +# Record of type 'A' (with wildcard) in 'helloworld.com' zone +# *.test5.helloworld.com. IN A 5.5.5.5 +apiVersion: dns.cav.enablers.ob/v1alpha1 +kind: RRset +metadata: + name: test5.helloworld.com +spec: + type: A + name: "*.test5" + ttl: 300 + records: + - 5.5.5.5 + zoneRef: + name: helloworld.com -# --- -# apiVersion: dns.cav.enablers.ob/v1alpha1 -# kind: RRset -# metadata: -# labels: -# app.kubernetes.io/name: powerdns-operator -# app.kubernetes.io/managed-by: kustomize -# name: test3.helloworld.com -# spec: -# type: A -# ttl: 300 -# records: -# - 1.1.1.1 -# zoneRef: -# name: helloworld.com +--- +# Record of type 'PTR' in 'helloworld.com' zone +# 1.1.168.192.in-addr.arpa. IN PTR mailserver.helloworld.com. +apiVersion: dns.cav.enablers.ob/v1alpha1 +kind: RRset +metadata: + name: 1.1.168.192.in-addr.arpa.helloworld.com +spec: + type: PTR + name: "1" + ttl: 300 + records: + - mailserver.helloworld.com. + zoneRef: + name: 1.168.192.in-addr.arpa -# --- -# apiVersion: dns.cav.enablers.ob/v1alpha1 -# kind: RRset -# metadata: -# labels: -# app.kubernetes.io/name: powerdns-operator -# app.kubernetes.io/managed-by: kustomize -# name: test4.helloworld.com -# spec: -# type: A -# ttl: 300 -# records: -# - 1.1.1.1 -# zoneRef: -# name: helloworld.com +--- +# Records of type 'MX' in 'helloworld.com' zone +# helloworld.com. IN MX 10 mailserver1.helloworld.com. +# helloworld.com. IN MX 20 mailserver2.helloworld.com. +apiVersion: dns.cav.enablers.ob/v1alpha1 +kind: RRset +metadata: + name: mx.helloworld.com +spec: + type: MX + name: "helloworld.com." + ttl: 300 + records: + - "10 mailserver1.helloworld.com." + - "20 mailserver2.helloworld.com." + zoneRef: + name: helloworld.com -# --- -# apiVersion: dns.cav.enablers.ob/v1alpha1 -# kind: RRset -# metadata: -# labels: -# app.kubernetes.io/name: powerdns-operator -# app.kubernetes.io/managed-by: kustomize -# name: test5.helloworld.com -# spec: -# type: A -# ttl: 300 -# records: -# - 1.1.1.1 -# zoneRef: -# name: helloworld.com +--- +# Record of type 'SRV' in 'helloworld.com' zone +# _database._tcp.myapp.helloworld.com. IN SRV 1 50 25565 test2.helloworld.com. +apiVersion: dns.cav.enablers.ob/v1alpha1 +kind: RRset +metadata: + name: database.srv.helloworld.com +spec: + type: SRV + name: "_database._tcp.myapp" + ttl: 300 + records: + - 1 50 25565 test2.helloworld.com. + zoneRef: + name: helloworld.com + +--- +# Record (duplicate) of type 'A' in 'helloworld.com' zone +# test1.helloworld.com. IN A 6.6.6.6 +apiVersion: dns.cav.enablers.ob/v1alpha1 +kind: RRset +metadata: + name: test1failed.helloworld.com +spec: + type: A + name: "test1" + ttl: 300 + records: + - "6.6.6.6" + zoneRef: + name: helloworld.com + +--- +# Record of type 'TXT' in 'helloworld.com' zone +# helloworld.com. IN TXT "Welcome to the helloworld.com domain" +apiVersion: dns.cav.enablers.ob/v1alpha1 +kind: RRset +metadata: + name: txt.helloworld.com +spec: + type: TXT + name: "helloworld.com." + ttl: 300 + records: + - "\"Welcome to the helloworld.com domain\"" + zoneRef: + name: helloworld.com + +--- +# Record (additional) of type 'A' in 'helloworld.com' zone +# test3.helloworld.com. IN A 192.168.1.7 +apiVersion: dns.cav.enablers.ob/v1alpha1 +kind: RRset +metadata: + name: test3-ipv4.helloworld.com +spec: + type: A + name: test3 + ttl: 300 + records: + - 192.168.1.7 + zoneRef: + name: helloworld.com + +--- +# Record (errored) of undefined type 'AA' in 'helloworld.com' zone +# test6.helloworld.com. IN A 192.168.1.7 +apiVersion: dns.cav.enablers.ob/v1alpha1 +kind: RRset +metadata: + name: test6.helloworld.com +spec: + type: AA + name: test6 + ttl: 300 + records: + - 192.168.1.7 + zoneRef: + name: helloworld.com + +--- +# Record of type 'A' in 'helloworld.com' zone +# test6.helloworld.com. IN A 192.168.1.7 +apiVersion: dns.cav.enablers.ob/v1alpha1 +kind: RRset +metadata: + name: test6-ok.helloworld.com +spec: + type: A + name: test6 + ttl: 300 + records: + - 192.168.1.7 + zoneRef: + name: helloworld.com diff --git a/config/samples/dns_v1alpha1_zone.yaml b/config/samples/dns_v1alpha1_zone.yaml index f7395f9..4745800 100644 --- a/config/samples/dns_v1alpha1_zone.yaml +++ b/config/samples/dns_v1alpha1_zone.yaml @@ -1,12 +1,23 @@ +--- +# Direct zone apiVersion: dns.cav.enablers.ob/v1alpha1 kind: Zone metadata: - labels: - app.kubernetes.io/name: powerdns-operator - app.kubernetes.io/managed-by: kustomize name: helloworld.com spec: nameservers: - ns1.helloworld.com - ns2.helloworld.com - kind: Native \ No newline at end of file + kind: Native + +--- +# Reverse Zone +apiVersion: dns.cav.enablers.ob/v1alpha1 +kind: Zone +metadata: + name: 1.168.192.in-addr.arpa +spec: + nameservers: + - ns1.helloworld.com + - ns2.helloworld.com + kind: Native diff --git a/docs/assets/logo.webp b/docs/assets/logo.webp deleted file mode 100644 index 2451331..0000000 Binary files a/docs/assets/logo.webp and /dev/null differ diff --git a/docs/guides/rrsets.md b/docs/guides/rrsets.md index 57c2743..a380bc1 100644 --- a/docs/guides/rrsets.md +++ b/docs/guides/rrsets.md @@ -7,6 +7,7 @@ The specification of the `RRset` contains the following fields: | Field | Type | Required | Description | | ----- | ---- |:--------:| ----------- | | type | string | Y | Type of the record (e.g. "A", "PTR", "MX") | +| name | string | Y | Name of the record | | ttl | uint32 | Y | DNS TTL of the records, in seconds | records | []string | Y | All records in this Resource Record Set | comment | string | N | Comment on RRSet | @@ -24,17 +25,17 @@ The specification of the `ZoneRef` contains the following fields: apiVersion: dns.cav.enablers.ob/v1alpha1 kind: RRset metadata: - labels: - app.kubernetes.io/name: powerdns-operator - app.kubernetes.io/managed-by: kustomize name: test.helloworld.com spec: comment: nothing to tell type: A + name: test ttl: 300 records: - 1.1.1.1 - 2.2.2.2 zoneRef: name: helloworld.com -``` \ No newline at end of file +``` + +> Note: The name can be canonical or not. \ No newline at end of file diff --git a/docs/guides/warnings.md b/docs/guides/warnings.md new file mode 100644 index 0000000..0a09640 --- /dev/null +++ b/docs/guides/warnings.md @@ -0,0 +1,48 @@ +# Warnings on field format + +## Deal with canonical names + +For some resources such as CNAME, PTR, MX, SRV, the records field MUST be in canonical format (end with a dot "."). See following examples. + +### CNAME + +```yaml +--8<-- "rrset-cname.yaml" +``` + +### PTR + +```yaml +--8<-- "rrset-ptr.yaml" +``` + +### MX + +```yaml +--8<-- "rrset-mx.yaml" +``` + +### SRV + +```yaml +--8<-- "rrset-srv.yaml" +``` + +## TXT Records + +Sometime, you may encounter the following error when applying a RRset custom resource: +```yaml +status: + syncErrorDescription: 'Record helloworld.com./TXT ''Welcome to the helloworld.com + domain'': Parsing record content (try ''pdnsutil check-zone''): Data field in + DNS should start with quote (") at position 0 of ''Welcome to the helloworld.com + domain''' + syncStatus: Failed +``` + +This error is due to a wrong format for the RRset. +TXT records MUST start AND end with an escaped quote (\"). See following example. + +```yaml +--8<-- "rrset-txt.yaml" +``` diff --git a/docs/guides/zones.md b/docs/guides/zones.md index de99b82..f453599 100644 --- a/docs/guides/zones.md +++ b/docs/guides/zones.md @@ -16,9 +16,6 @@ The specification of the `Zone` contains the following fields: apiVersion: dns.cav.enablers.ob/v1alpha1 kind: Zone metadata: - labels: - app.kubernetes.io/name: powerdns-operator - app.kubernetes.io/managed-by: kustomize name: helloworld.com spec: nameservers: diff --git a/docs/snippets/rrset-cname.yaml b/docs/snippets/rrset-cname.yaml new file mode 100644 index 0000000..dd3c771 --- /dev/null +++ b/docs/snippets/rrset-cname.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: dns.cav.enablers.ob/v1alpha1 +kind: RRset +metadata: + name: test4.helloworld.com +spec: + type: CNAME + name: test4 + ttl: 300 + records: + - test1.helloworld.com. + zoneRef: + name: helloworld.com \ No newline at end of file diff --git a/docs/snippets/rrset-mx.yaml b/docs/snippets/rrset-mx.yaml new file mode 100644 index 0000000..d680033 --- /dev/null +++ b/docs/snippets/rrset-mx.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: dns.cav.enablers.ob/v1alpha1 +kind: RRset +metadata: + name: mx.helloworld.com +spec: + type: MX + name: "helloworld.com." + ttl: 300 + records: + - "10 mailserver1.helloworld.com." + - "20 mailserver2.helloworld.com." + zoneRef: + name: helloworld.com \ No newline at end of file diff --git a/docs/snippets/rrset-ptr.yaml b/docs/snippets/rrset-ptr.yaml new file mode 100644 index 0000000..d9f27be --- /dev/null +++ b/docs/snippets/rrset-ptr.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: dns.cav.enablers.ob/v1alpha1 +kind: RRset +metadata: + name: 1.1.168.192.in-addr.arpa.helloworld.com +spec: + type: PTR + name: "1" + ttl: 300 + records: + - mailserver.helloworld.com. + zoneRef: + name: 1.168.192.in-addr.arpa \ No newline at end of file diff --git a/docs/snippets/rrset-srv.yaml b/docs/snippets/rrset-srv.yaml new file mode 100644 index 0000000..91c14df --- /dev/null +++ b/docs/snippets/rrset-srv.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: dns.cav.enablers.ob/v1alpha1 +kind: RRset +metadata: + name: database.srv.helloworld.com +spec: + type: SRV + name: "_database._tcp.myapp" + ttl: 300 + records: + - 1 50 25565 test2.helloworld.com. + zoneRef: + name: helloworld.com \ No newline at end of file diff --git a/docs/snippets/rrset-txt.yaml b/docs/snippets/rrset-txt.yaml new file mode 100644 index 0000000..a6b1787 --- /dev/null +++ b/docs/snippets/rrset-txt.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: dns.cav.enablers.ob/v1alpha1 +kind: RRset +metadata: + name: txt.helloworld.com +spec: + type: TXT + name: "helloworld.com." + ttl: 300 + records: + - "\"Welcome to the helloworld.com domain\"" + zoneRef: + name: helloworld.com \ No newline at end of file diff --git a/docs/snippets/rrset.yaml b/docs/snippets/rrset.yaml deleted file mode 100644 index 083d6ec..0000000 --- a/docs/snippets/rrset.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: dns.cav.enablers.ob/v1alpha1 -kind: RRset -metadata: - labels: - app.kubernetes.io/name: powerdns-operator - app.kubernetes.io/managed-by: kustomize - name: test.helloworld.com -spec: - comment: nothing to tell - type: A - ttl: 300 - records: - - 1.1.1.1 - - 2.2.2.2 - zoneRef: - name: helloworld.com \ No newline at end of file diff --git a/internal/controller/pdns_helper.go b/internal/controller/pdns_helper.go index 98db711..fde91de 100644 --- a/internal/controller/pdns_helper.go +++ b/internal/controller/pdns_helper.go @@ -66,9 +66,17 @@ func rrsetIsIdenticalToExternalRRset(rrset *dnsv1alpha1.RRset, externalRecord po for _, r := range externalRecord.Records { externalRecordsSlice = append(externalRecordsSlice, *r.Content) } - return makeCanonical(rrset.ObjectMeta.Name) == *externalRecord.Name && rrset.Spec.Type == string(*externalRecord.Type) && rrset.Spec.TTL == *(externalRecord.TTL) && commentsIdentical && reflect.DeepEqual(rrset.Spec.Records, externalRecordsSlice) + name := getRRsetName(rrset) + return name == *externalRecord.Name && rrset.Spec.Type == string(*externalRecord.Type) && rrset.Spec.TTL == *(externalRecord.TTL) && commentsIdentical && reflect.DeepEqual(rrset.Spec.Records, externalRecordsSlice) } func makeCanonical(in string) string { return fmt.Sprintf("%s.", strings.TrimSuffix(in, ".")) } + +func getRRsetName(rrset *dnsv1alpha1.RRset) string { + if !strings.HasSuffix(rrset.Spec.Name, ".") { + return makeCanonical(rrset.Spec.Name + "." + rrset.Spec.ZoneRef.Name) + } + return makeCanonical(rrset.Spec.Name) +} diff --git a/internal/controller/rrset_controller.go b/internal/controller/rrset_controller.go index 961d7c9..893ba62 100644 --- a/internal/controller/rrset_controller.go +++ b/internal/controller/rrset_controller.go @@ -16,6 +16,7 @@ import ( "github.com/joeig/go-powerdns/v3" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" @@ -27,6 +28,11 @@ import ( dnsv1alpha1 "github.com/orange-opensource/powerdns-operator/api/v1alpha1" ) +const ( + FAILED_STATUS = "Failed" + SUCCEEDED_STATUS = "Succeeded" +) + // RRsetReconciler reconciles a RRset object type RRsetReconciler struct { client.Client @@ -47,6 +53,11 @@ func (r *RRsetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl if err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } + isInFailedStatus := (rrset.Status.SyncStatus != nil && *rrset.Status.SyncStatus == FAILED_STATUS) + + // initialize syncStatus + var syncStatus *string + var syncErrorDescription *string // Retrieve lastUpdateTime if defined, otherwise Now() lastUpdateTime := &metav1.Time{Time: time.Now().UTC()} @@ -94,13 +105,14 @@ func (r *RRsetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl // The object is being deleted if controllerutil.ContainsFinalizer(rrset, FINALIZER_NAME) { // our finalizer is present, so lets handle any external dependency - if err := r.deleteExternalResources(ctx, zone, rrset); err != nil { - // if fail to delete the external resource, return with error - // so that it can be retried - log.Error(err, "Failed to delete external resources") - return ctrl.Result{}, err + if !isInFailedStatus { + if err := r.deleteExternalResources(ctx, zone, rrset); err != nil { + // if fail to delete the external resource, return with error + // so that it can be retried + log.Error(err, "Failed to delete external resources") + return ctrl.Result{}, err + } } - // remove our finalizer from the list and update it. controllerutil.RemoveFinalizer(rrset, FINALIZER_NAME) if err := r.Update(ctx, rrset); err != nil { @@ -115,11 +127,39 @@ func (r *RRsetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl return ctrl.Result{}, nil } + // We cannot exit previously (at the early moments of reconcile), because we have to allow deletion process + if isInFailedStatus { + return ctrl.Result{}, nil + } + + // If a RRset already exists with the same DNS name: + // * Stop reconciliation + // * Append a Failed Status on RRset + var existingRRsets dnsv1alpha1.RRsetList + if err := r.List(ctx, &existingRRsets, client.MatchingFields{"DNS.Entry.Name": getRRsetName(rrset) + "/" + rrset.Spec.Type}); err != nil { + log.Error(err, "unable to find RRsets related to the DNS Name") + return ctrl.Result{}, err + } + if len(existingRRsets.Items) > 1 { + original := rrset.DeepCopy() + rrset.Status.LastUpdateTime = lastUpdateTime + name := getRRsetName(rrset) + rrset.Status.DnsEntryName = &name + rrset.Status.SyncStatus = ptr.To(FAILED_STATUS) + rrset.Status.SyncErrorDescription = ptr.To("Already existing RRset with the same FQDN") + if err := r.Status().Patch(ctx, rrset, client.MergeFrom(original)); err != nil { + log.Error(err, "unable to patch RRSet status") + return ctrl.Result{}, err + } + return ctrl.Result{}, nil + } + // Create or Update changed, err := r.createOrUpdateExternalResources(ctx, zone, rrset) if err != nil { log.Error(err, "Failed to create or update external resources") - return ctrl.Result{}, err + syncStatus = ptr.To(FAILED_STATUS) + syncErrorDescription = ptr.To(err.Error()) } if changed { lastUpdateTime = &metav1.Time{Time: time.Now().UTC()} @@ -141,7 +181,15 @@ func (r *RRsetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl // In that case, the Serial in Zone Status is false // This update permits triggering a new event after RRSet update applied original := rrset.DeepCopy() + if syncStatus == nil { + syncStatus = ptr.To(SUCCEEDED_STATUS) + } rrset.Status.LastUpdateTime = lastUpdateTime + rrset.Status.DnsEntryName = ptr.To(getRRsetName(rrset)) + rrset.Status.SyncStatus = syncStatus + if syncErrorDescription != nil { + rrset.Status.SyncErrorDescription = syncErrorDescription + } if err := r.Status().Patch(ctx, rrset, client.MergeFrom(original)); err != nil { log.Error(err, "unable to patch RRSet status") return ctrl.Result{}, err @@ -152,6 +200,17 @@ func (r *RRsetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl // 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(), &dnsv1alpha1.RRset{}, "DNS.Entry.Name", func(rawObj client.Object) []string { + // grab the RRset object, extract its name... + var RRsetName string + if rawObj.(*dnsv1alpha1.RRset).Status.SyncStatus == nil || *rawObj.(*dnsv1alpha1.RRset).Status.SyncStatus == SUCCEEDED_STATUS { + RRsetName = getRRsetName(rawObj.(*dnsv1alpha1.RRset)) + "/" + rawObj.(*dnsv1alpha1.RRset).Spec.Type + } + return []string{RRsetName} + }); err != nil { + return err + } return ctrl.NewControllerManagedBy(mgr). For(&dnsv1alpha1.RRset{}). Complete(r) @@ -161,7 +220,7 @@ func (r *RRsetReconciler) deleteExternalResources(ctx context.Context, zone *dns log := log.FromContext(ctx) // Delete - err := r.PDNSClient.Records.Delete(ctx, zone.ObjectMeta.Name, rrset.ObjectMeta.Name, powerdns.RRType(rrset.Spec.Type)) + err := r.PDNSClient.Records.Delete(ctx, zone.ObjectMeta.Name, getRRsetName(rrset), powerdns.RRType(rrset.Spec.Type)) if err != nil { log.Error(err, "Failed to delete record") return err @@ -172,13 +231,11 @@ func (r *RRsetReconciler) deleteExternalResources(ctx context.Context, zone *dns // createOrUpdateExternalResources create or update the input resource if necessary, and return True if changed func (r *RRsetReconciler) createOrUpdateExternalResources(ctx context.Context, zone *dnsv1alpha1.Zone, rrset *dnsv1alpha1.RRset) (bool, error) { - log := log.FromContext(ctx) - + name := getRRsetName(rrset) rrType := powerdns.RRType(rrset.Spec.Type) // Looking for a record with same Name and Type - records, err := r.PDNSClient.Records.Get(ctx, zone.ObjectMeta.Name, rrset.ObjectMeta.Name, &rrType) + records, err := r.PDNSClient.Records.Get(ctx, zone.ObjectMeta.Name, name, &rrType) if err != nil && !errors.IsNotFound(err) { - log.Error(err, "Failed to get external rrsets for the type") return false, err } // An issue exist on GET API Calls, comments for another RRSet are included although we filter @@ -186,7 +243,7 @@ func (r *RRsetReconciler) createOrUpdateExternalResources(ctx context.Context, z // See https://github.com/PowerDNS/pdns/pull/14045 var filteredRecord powerdns.RRset for _, fr := range records { - if *fr.Name == makeCanonical(rrset.ObjectMeta.Name) { + if *fr.Name == makeCanonical(name) { filteredRecord = fr break } @@ -201,9 +258,8 @@ func (r *RRsetReconciler) createOrUpdateExternalResources(ctx context.Context, z if rrset.Spec.Comment != nil { comments = powerdns.WithComments(powerdns.Comment{Content: rrset.Spec.Comment, Account: &operatorAccount}) } - err = r.PDNSClient.Records.Change(ctx, zone.ObjectMeta.Name, rrset.ObjectMeta.Name, rrType, rrset.Spec.TTL, rrset.Spec.Records, comments) + err = r.PDNSClient.Records.Change(ctx, zone.ObjectMeta.Name, name, rrType, rrset.Spec.TTL, rrset.Spec.Records, comments) if err != nil { - log.Error(err, "Failed to create record") return false, err } diff --git a/internal/controller/rrset_controller_test.go b/internal/controller/rrset_controller_test.go index 31015bb..5e62e54 100644 --- a/internal/controller/rrset_controller_test.go +++ b/internal/controller/rrset_controller_test.go @@ -38,6 +38,7 @@ var _ = Describe("RRset Controller", func() { // RRset resourceName = "test.example2.org" resourceNamespace = "default" + resourceDNSName = "test" resourceTTL = uint32(300) resourceType = "A" resourceComment = "Just a comment" @@ -111,6 +112,7 @@ var _ = Describe("RRset Controller", func() { Name: zoneRef, }, Type: resourceType, + Name: resourceDNSName, TTL: resourceTTL, Records: resourceRecords, Comment: &comment, @@ -336,6 +338,7 @@ var _ = Describe("RRset Controller", func() { // Specific test variables recreationResourceName := "test2.example2.org" recreationResourceNamespace := "default" + recreationResourceDNSName := "test2" recreationResourceTTL := uint32(253) recreationResourceType := "A" recreationResourceComment := "it is an useless comment" @@ -367,6 +370,7 @@ var _ = Describe("RRset Controller", func() { resource.Spec = dnsv1alpha1.RRsetSpec{ Type: recreationResourceType, TTL: recreationResourceTTL, + Name: recreationResourceDNSName, Records: []string{recreationRecord}, Comment: &recreationResourceComment, ZoneRef: dnsv1alpha1.ZoneRef{ @@ -437,6 +441,7 @@ var _ = Describe("RRset Controller", func() { } _, err := controllerutil.CreateOrUpdate(ctx, k8sClient, resource, func() error { resource.Spec.TTL = modifiedResourceTTL + resource.Spec.Name = resourceDNSName resource.Spec.Comment = &modifiedResourceComment resource.Spec.Records = modifiedResourceRecords return nil @@ -469,6 +474,7 @@ var _ = Describe("RRset Controller", func() { By("Creating a RRset") fakeResourceName := "fake.example2.org" fakeResourceNamespace := "default" + fakeResourceDNSName := "fake" fakeResourceTTL := uint32(123) fakeResourceType := "A" fakeResourceComment := "it is a fake comment" @@ -485,6 +491,7 @@ var _ = Describe("RRset Controller", func() { _, err := controllerutil.CreateOrUpdate(ctx, k8sClient, fakeResource, func() error { fakeResource.Spec = dnsv1alpha1.RRsetSpec{ Type: fakeResourceType, + Name: fakeResourceDNSName, TTL: fakeResourceTTL, Records: fakeRecords, Comment: &fakeResourceComment, @@ -520,4 +527,537 @@ var _ = Describe("RRset Controller", func() { }, timeout, interval).Should(BeTrue()) }) }) + + Context("When creating RRset", func() { + It("should successfully reconcile the resource", Label("rrset-creation", "AAAA-Type"), func() { + ctx := context.Background() + // Specific test variables + additionalResourceName := "aaaa" + additionalResourceType := "AAAA" + additionalResourceRecords := []string{"2001:0dc8:86a4:0000:0000:7a2f:2360:2341"} + additionalResourceComment := "This is a AAAA Record" + + By("Creating the RRset resource") + additionalResource := &dnsv1alpha1.RRset{ + ObjectMeta: metav1.ObjectMeta{ + Name: additionalResourceName, + Namespace: resourceNamespace, + }, + } + additionalResource.SetResourceVersion("") + _, err := controllerutil.CreateOrUpdate(ctx, k8sClient, additionalResource, func() error { + additionalResource.Spec = dnsv1alpha1.RRsetSpec{ + ZoneRef: dnsv1alpha1.ZoneRef{ + Name: zoneRef, + }, + Type: additionalResourceType, + Name: additionalResourceName, + TTL: resourceTTL, + Records: additionalResourceRecords, + Comment: &additionalResourceComment, + } + return nil + }) + additionalRRsetLookupKey := types.NamespacedName{ + Name: additionalResourceName, + Namespace: resourceNamespace, + } + + Expect(err).NotTo(HaveOccurred()) + Eventually(func() bool { + err := k8sClient.Get(ctx, additionalRRsetLookupKey, additionalResource) + return err == nil + }, timeout, interval).Should(BeTrue()) + // Confirm that resource is created in the backend + DnsFqdn := getRRsetName(additionalResource) + Eventually(func() bool { + _, ok := readFromRecordsMap(makeCanonical(DnsFqdn)) + return ok + }, timeout, interval).Should(BeTrue()) + + By("Getting the created resource") + createdResource := &dnsv1alpha1.RRset{} + Eventually(func() bool { + err := k8sClient.Get(ctx, additionalRRsetLookupKey, createdResource) + return err == nil + }, timeout, interval).Should(BeTrue()) + + Expect(getMockedRecordsForType(DnsFqdn, additionalResourceType)).To(Equal(additionalResourceRecords)) + Expect(getMockedTTL(DnsFqdn, additionalResourceType)).To(Equal(resourceTTL)) + Expect(getMockedComment(DnsFqdn, additionalResourceType)).To(Equal(additionalResourceComment)) + Expect(createdResource.GetOwnerReferences()).NotTo(BeEmpty(), "RRset should have setOwnerReference") + Expect(createdResource.GetOwnerReferences()[0].Name).To(Equal(zoneRef), "RRset should have setOwnerReference to Zone") + Expect(createdResource.GetFinalizers()).To(ContainElement(FINALIZER_NAME), "RRset should contain the finalizer") + + }) + }) + + Context("When creating RRset", func() { + It("should successfully reconcile the resource", Label("rrset-creation", "CNAME-Type"), func() { + ctx := context.Background() + // Specific test variables + additionalResourceName := "cname" + additionalResourceType := "CNAME" + additionalResourceRecords := []string{resourceName} + additionalResourceComment := "This is a CNAME Record" + + By("Creating the RRset resource") + additionalResource := &dnsv1alpha1.RRset{ + ObjectMeta: metav1.ObjectMeta{ + Name: additionalResourceName, + Namespace: resourceNamespace, + }, + } + additionalResource.SetResourceVersion("") + _, err := controllerutil.CreateOrUpdate(ctx, k8sClient, additionalResource, func() error { + additionalResource.Spec = dnsv1alpha1.RRsetSpec{ + ZoneRef: dnsv1alpha1.ZoneRef{ + Name: zoneRef, + }, + Type: additionalResourceType, + Name: additionalResourceName, + TTL: resourceTTL, + Records: additionalResourceRecords, + Comment: &additionalResourceComment, + } + return nil + }) + additionalRRsetLookupKey := types.NamespacedName{ + Name: additionalResourceName, + Namespace: resourceNamespace, + } + + Expect(err).NotTo(HaveOccurred()) + Eventually(func() bool { + err := k8sClient.Get(ctx, additionalRRsetLookupKey, additionalResource) + return err == nil + }, timeout, interval).Should(BeTrue()) + // Confirm that resource is created in the backend + DnsFqdn := getRRsetName(additionalResource) + Eventually(func() bool { + _, ok := readFromRecordsMap(makeCanonical(DnsFqdn)) + return ok + }, timeout, interval).Should(BeTrue()) + + By("Getting the created resource") + createdResource := &dnsv1alpha1.RRset{} + Eventually(func() bool { + err := k8sClient.Get(ctx, additionalRRsetLookupKey, createdResource) + return err == nil + }, timeout, interval).Should(BeTrue()) + + Expect(getMockedRecordsForType(DnsFqdn, additionalResourceType)).To(Equal(additionalResourceRecords)) + Expect(getMockedTTL(DnsFqdn, additionalResourceType)).To(Equal(resourceTTL)) + Expect(getMockedComment(DnsFqdn, additionalResourceType)).To(Equal(additionalResourceComment)) + Expect(createdResource.GetOwnerReferences()).NotTo(BeEmpty(), "RRset should have setOwnerReference") + Expect(createdResource.GetOwnerReferences()[0].Name).To(Equal(zoneRef), "RRset should have setOwnerReference to Zone") + Expect(createdResource.GetFinalizers()).To(ContainElement(FINALIZER_NAME), "RRset should contain the finalizer") + }) + }) + + Context("When creating RRset", func() { + It("should successfully reconcile the resource", Label("rrset-creation", "Wildcard-Type"), func() { + ctx := context.Background() + // Specific test variables + additionalResourceName := "wildcard" + additionalResourceType := "*.a" + additionalResourceRecords := []string{"192.168.1.123"} + additionalResourceComment := "This is a A Wildcard Record" + + By("Creating the RRset resource") + additionalResource := &dnsv1alpha1.RRset{ + ObjectMeta: metav1.ObjectMeta{ + Name: additionalResourceName, + Namespace: resourceNamespace, + }, + } + additionalResource.SetResourceVersion("") + _, err := controllerutil.CreateOrUpdate(ctx, k8sClient, additionalResource, func() error { + additionalResource.Spec = dnsv1alpha1.RRsetSpec{ + ZoneRef: dnsv1alpha1.ZoneRef{ + Name: zoneRef, + }, + Type: additionalResourceType, + Name: additionalResourceName, + TTL: resourceTTL, + Records: additionalResourceRecords, + Comment: &additionalResourceComment, + } + return nil + }) + additionalRRsetLookupKey := types.NamespacedName{ + Name: additionalResourceName, + Namespace: resourceNamespace, + } + + Expect(err).NotTo(HaveOccurred()) + Eventually(func() bool { + err := k8sClient.Get(ctx, additionalRRsetLookupKey, additionalResource) + return err == nil + }, timeout, interval).Should(BeTrue()) + // Confirm that resource is created in the backend + DnsFqdn := getRRsetName(additionalResource) + Eventually(func() bool { + _, ok := readFromRecordsMap(makeCanonical(DnsFqdn)) + return ok + }, timeout, interval).Should(BeTrue()) + + By("Getting the created resource") + createdResource := &dnsv1alpha1.RRset{} + Eventually(func() bool { + err := k8sClient.Get(ctx, additionalRRsetLookupKey, createdResource) + return err == nil + }, timeout, interval).Should(BeTrue()) + + Expect(getMockedRecordsForType(DnsFqdn, additionalResourceType)).To(Equal(additionalResourceRecords)) + Expect(getMockedTTL(DnsFqdn, additionalResourceType)).To(Equal(resourceTTL)) + Expect(getMockedComment(DnsFqdn, additionalResourceType)).To(Equal(additionalResourceComment)) + Expect(createdResource.GetOwnerReferences()).NotTo(BeEmpty(), "RRset should have setOwnerReference") + Expect(createdResource.GetOwnerReferences()[0].Name).To(Equal(zoneRef), "RRset should have setOwnerReference to Zone") + Expect(createdResource.GetFinalizers()).To(ContainElement(FINALIZER_NAME), "RRset should contain the finalizer") + }) + }) + Context("When creating RRset", func() { + It("should successfully reconcile the resource", Label("rrset-creation", "MX-Type"), func() { + ctx := context.Background() + // Specific test variables + additionalResourceName := "mx" + additionalResourceType := makeCanonical(zoneRef) + additionalResourceRecords := []string{"10 mail1.example2.org.", "20 mail2.example2.org."} + additionalResourceComment := "This is a MX Record" + + By("Creating the RRset resource") + additionalResource := &dnsv1alpha1.RRset{ + ObjectMeta: metav1.ObjectMeta{ + Name: additionalResourceName, + Namespace: resourceNamespace, + }, + } + additionalResource.SetResourceVersion("") + _, err := controllerutil.CreateOrUpdate(ctx, k8sClient, additionalResource, func() error { + additionalResource.Spec = dnsv1alpha1.RRsetSpec{ + ZoneRef: dnsv1alpha1.ZoneRef{ + Name: zoneRef, + }, + Type: additionalResourceType, + Name: additionalResourceName, + TTL: resourceTTL, + Records: additionalResourceRecords, + Comment: &additionalResourceComment, + } + return nil + }) + additionalRRsetLookupKey := types.NamespacedName{ + Name: additionalResourceName, + Namespace: resourceNamespace, + } + + Expect(err).NotTo(HaveOccurred()) + Eventually(func() bool { + err := k8sClient.Get(ctx, additionalRRsetLookupKey, additionalResource) + return err == nil + }, timeout, interval).Should(BeTrue()) + // Confirm that resource is created in the backend + DnsFqdn := getRRsetName(additionalResource) + Eventually(func() bool { + _, ok := readFromRecordsMap(makeCanonical(DnsFqdn)) + return ok + }, timeout, interval).Should(BeTrue()) + + By("Getting the created resource") + createdResource := &dnsv1alpha1.RRset{} + Eventually(func() bool { + err := k8sClient.Get(ctx, additionalRRsetLookupKey, createdResource) + return err == nil + }, timeout, interval).Should(BeTrue()) + + Expect(getMockedRecordsForType(DnsFqdn, additionalResourceType)).To(Equal(additionalResourceRecords)) + Expect(getMockedTTL(DnsFqdn, additionalResourceType)).To(Equal(resourceTTL)) + Expect(getMockedComment(DnsFqdn, additionalResourceType)).To(Equal(additionalResourceComment)) + Expect(createdResource.GetOwnerReferences()).NotTo(BeEmpty(), "RRset should have setOwnerReference") + Expect(createdResource.GetOwnerReferences()[0].Name).To(Equal(zoneRef), "RRset should have setOwnerReference to Zone") + Expect(createdResource.GetFinalizers()).To(ContainElement(FINALIZER_NAME), "RRset should contain the finalizer") + }) + }) + + Context("When creating RRset", func() { + It("should successfully reconcile the resource", Label("rrset-creation", "NS-Type"), func() { + ctx := context.Background() + // Specific test variables + additionalResourceName := "ns" + additionalResourceType := makeCanonical(zoneRef) + additionalResourceRecords := []string{"ns1.example2.org", "ns2.example2.org"} + additionalResourceComment := "This is a NS Record" + + By("Creating the RRset resource") + additionalResource := &dnsv1alpha1.RRset{ + ObjectMeta: metav1.ObjectMeta{ + Name: additionalResourceName, + Namespace: resourceNamespace, + }, + } + additionalResource.SetResourceVersion("") + _, err := controllerutil.CreateOrUpdate(ctx, k8sClient, additionalResource, func() error { + additionalResource.Spec = dnsv1alpha1.RRsetSpec{ + ZoneRef: dnsv1alpha1.ZoneRef{ + Name: zoneRef, + }, + Type: additionalResourceType, + Name: additionalResourceName, + TTL: resourceTTL, + Records: additionalResourceRecords, + Comment: &additionalResourceComment, + } + return nil + }) + additionalRRsetLookupKey := types.NamespacedName{ + Name: additionalResourceName, + Namespace: resourceNamespace, + } + + Expect(err).NotTo(HaveOccurred()) + Eventually(func() bool { + err := k8sClient.Get(ctx, additionalRRsetLookupKey, additionalResource) + return err == nil + }, timeout, interval).Should(BeTrue()) + // Confirm that resource is created in the backend + DnsFqdn := getRRsetName(additionalResource) + Eventually(func() bool { + _, ok := readFromRecordsMap(makeCanonical(DnsFqdn)) + return ok + }, timeout, interval).Should(BeTrue()) + + By("Getting the created resource") + createdResource := &dnsv1alpha1.RRset{} + Eventually(func() bool { + err := k8sClient.Get(ctx, additionalRRsetLookupKey, createdResource) + return err == nil + }, timeout, interval).Should(BeTrue()) + + Expect(getMockedRecordsForType(DnsFqdn, additionalResourceType)).To(Equal(additionalResourceRecords)) + Expect(getMockedTTL(DnsFqdn, additionalResourceType)).To(Equal(resourceTTL)) + Expect(getMockedComment(DnsFqdn, additionalResourceType)).To(Equal(additionalResourceComment)) + Expect(createdResource.GetOwnerReferences()).NotTo(BeEmpty(), "RRset should have setOwnerReference") + Expect(createdResource.GetOwnerReferences()[0].Name).To(Equal(zoneRef), "RRset should have setOwnerReference to Zone") + Expect(createdResource.GetFinalizers()).To(ContainElement(FINALIZER_NAME), "RRset should contain the finalizer") + }) + }) + + Context("When creating RRset", func() { + It("should successfully reconcile the resource", Label("rrset-creation", "TXT-Type"), func() { + ctx := context.Background() + // Specific test variables + additionalResourceName := "txt" + additionalResourceType := makeCanonical(zoneRef) + additionalResourceRecords := []string{"This a TXT Record"} + additionalResourceComment := "This is a TXT Record" + + By("Creating the RRset resource") + additionalResource := &dnsv1alpha1.RRset{ + ObjectMeta: metav1.ObjectMeta{ + Name: additionalResourceName, + Namespace: resourceNamespace, + }, + } + additionalResource.SetResourceVersion("") + _, err := controllerutil.CreateOrUpdate(ctx, k8sClient, additionalResource, func() error { + additionalResource.Spec = dnsv1alpha1.RRsetSpec{ + ZoneRef: dnsv1alpha1.ZoneRef{ + Name: zoneRef, + }, + Type: additionalResourceType, + Name: additionalResourceName, + TTL: resourceTTL, + Records: additionalResourceRecords, + Comment: &additionalResourceComment, + } + return nil + }) + additionalRRsetLookupKey := types.NamespacedName{ + Name: additionalResourceName, + Namespace: resourceNamespace, + } + + Expect(err).NotTo(HaveOccurred()) + Eventually(func() bool { + err := k8sClient.Get(ctx, additionalRRsetLookupKey, additionalResource) + return err == nil + }, timeout, interval).Should(BeTrue()) + // Confirm that resource is created in the backend + DnsFqdn := getRRsetName(additionalResource) + Eventually(func() bool { + _, ok := readFromRecordsMap(makeCanonical(DnsFqdn)) + return ok + }, timeout, interval).Should(BeTrue()) + + By("Getting the created resource") + createdResource := &dnsv1alpha1.RRset{} + Eventually(func() bool { + err := k8sClient.Get(ctx, additionalRRsetLookupKey, createdResource) + return err == nil + }, timeout, interval).Should(BeTrue()) + + Expect(getMockedRecordsForType(DnsFqdn, additionalResourceType)).To(Equal(additionalResourceRecords)) + Expect(getMockedTTL(DnsFqdn, additionalResourceType)).To(Equal(resourceTTL)) + Expect(getMockedComment(DnsFqdn, additionalResourceType)).To(Equal(additionalResourceComment)) + Expect(createdResource.GetOwnerReferences()).NotTo(BeEmpty(), "RRset should have setOwnerReference") + Expect(createdResource.GetOwnerReferences()[0].Name).To(Equal(zoneRef), "RRset should have setOwnerReference to Zone") + Expect(createdResource.GetFinalizers()).To(ContainElement(FINALIZER_NAME), "RRset should contain the finalizer") + }) + }) + + Context("When creating RRset", func() { + It("should successfully reconcile the resource", Label("rrset-creation", "SRV-Type"), func() { + ctx := context.Background() + // Specific test variables + additionalResourceName := "srv" + additionalResourceType := "_srv._protcol.myapp" + additionalResourceRecords := []string{"1 50 25565 front.example2.org."} + additionalResourceComment := "This is a SRV Record" + + By("Creating the RRset resource") + additionalResource := &dnsv1alpha1.RRset{ + ObjectMeta: metav1.ObjectMeta{ + Name: additionalResourceName, + Namespace: resourceNamespace, + }, + } + additionalResource.SetResourceVersion("") + _, err := controllerutil.CreateOrUpdate(ctx, k8sClient, additionalResource, func() error { + additionalResource.Spec = dnsv1alpha1.RRsetSpec{ + ZoneRef: dnsv1alpha1.ZoneRef{ + Name: zoneRef, + }, + Type: additionalResourceType, + Name: additionalResourceName, + TTL: resourceTTL, + Records: additionalResourceRecords, + Comment: &additionalResourceComment, + } + return nil + }) + additionalRRsetLookupKey := types.NamespacedName{ + Name: additionalResourceName, + Namespace: resourceNamespace, + } + + Expect(err).NotTo(HaveOccurred()) + Eventually(func() bool { + err := k8sClient.Get(ctx, additionalRRsetLookupKey, additionalResource) + return err == nil + }, timeout, interval).Should(BeTrue()) + // Confirm that resource is created in the backend + DnsFqdn := getRRsetName(additionalResource) + Eventually(func() bool { + _, ok := readFromRecordsMap(makeCanonical(DnsFqdn)) + return ok + }, timeout, interval).Should(BeTrue()) + + By("Getting the created resource") + createdResource := &dnsv1alpha1.RRset{} + Eventually(func() bool { + err := k8sClient.Get(ctx, additionalRRsetLookupKey, createdResource) + return err == nil + }, timeout, interval).Should(BeTrue()) + + Expect(getMockedRecordsForType(DnsFqdn, additionalResourceType)).To(Equal(additionalResourceRecords)) + Expect(getMockedTTL(DnsFqdn, additionalResourceType)).To(Equal(resourceTTL)) + Expect(getMockedComment(DnsFqdn, additionalResourceType)).To(Equal(additionalResourceComment)) + Expect(createdResource.GetOwnerReferences()).NotTo(BeEmpty(), "RRset should have setOwnerReference") + Expect(createdResource.GetOwnerReferences()[0].Name).To(Equal(zoneRef), "RRset should have setOwnerReference to Zone") + Expect(createdResource.GetFinalizers()).To(ContainElement(FINALIZER_NAME), "RRset should contain the finalizer") + }) + }) + + Context("When creating RRset", func() { + It("should successfully reconcile the resource", Label("rrset-creation", "PTR-Type"), func() { + ctx := context.Background() + // Specific test variables + reverseZoneName := "123.168.192.in-addr.arpa" + additionalResourceName := "ptr" + additionalResourceType := "1" + additionalResourceRecords := []string{"mail1.example2.org"} + additionalResourceComment := "This is a PTR Record" + + By("Creating the Reverse Zone resource") + reverseZone := &dnsv1alpha1.Zone{ + ObjectMeta: metav1.ObjectMeta{ + Name: reverseZoneName, + }, + } + reverseZone.SetResourceVersion("") + _, err := controllerutil.CreateOrUpdate(ctx, k8sClient, reverseZone, func() error { + reverseZone.Spec = dnsv1alpha1.ZoneSpec{ + Kind: zoneKind, + Nameservers: []string{zoneNS1, zoneNS2}, + } + return nil + }) + Expect(err).NotTo(HaveOccurred()) + additionalReverseZoneLookupKey := types.NamespacedName{ + Name: reverseZoneName, + } + Eventually(func() bool { + err := k8sClient.Get(ctx, additionalReverseZoneLookupKey, reverseZone) + return err == nil + }, timeout, interval).Should(BeTrue()) + // Confirm that resource is created in the backend + Eventually(func() bool { + _, found := readFromZonesMap(makeCanonical(reverseZone.Name)) + return found + }, timeout, interval).Should(BeTrue()) + + By("Creating the RRset resource") + additionalResource := &dnsv1alpha1.RRset{ + ObjectMeta: metav1.ObjectMeta{ + Name: additionalResourceName, + Namespace: resourceNamespace, + }, + } + additionalResource.SetResourceVersion("") + _, err = controllerutil.CreateOrUpdate(ctx, k8sClient, additionalResource, func() error { + additionalResource.Spec = dnsv1alpha1.RRsetSpec{ + ZoneRef: dnsv1alpha1.ZoneRef{ + Name: reverseZoneName, + }, + Type: additionalResourceType, + Name: additionalResourceName, + TTL: resourceTTL, + Records: additionalResourceRecords, + Comment: &additionalResourceComment, + } + return nil + }) + additionalRRsetLookupKey := types.NamespacedName{ + Name: additionalResourceName, + Namespace: resourceNamespace, + } + + Expect(err).NotTo(HaveOccurred()) + Eventually(func() bool { + err := k8sClient.Get(ctx, additionalRRsetLookupKey, additionalResource) + return err == nil + }, timeout, interval).Should(BeTrue()) + // Confirm that resource is created in the backend + DnsFqdn := getRRsetName(additionalResource) + Eventually(func() bool { + _, ok := readFromRecordsMap(makeCanonical(DnsFqdn)) + return ok + }, timeout, interval).Should(BeTrue()) + + By("Getting the created resource") + createdResource := &dnsv1alpha1.RRset{} + Eventually(func() bool { + err := k8sClient.Get(ctx, additionalRRsetLookupKey, createdResource) + return err == nil + }, timeout, interval).Should(BeTrue()) + + Expect(getMockedRecordsForType(DnsFqdn, additionalResourceType)).To(Equal(additionalResourceRecords)) + Expect(getMockedTTL(DnsFqdn, additionalResourceType)).To(Equal(resourceTTL)) + Expect(getMockedComment(DnsFqdn, additionalResourceType)).To(Equal(additionalResourceComment)) + Expect(createdResource.GetOwnerReferences()).NotTo(BeEmpty(), "RRset should have setOwnerReference") + Expect(createdResource.GetOwnerReferences()[0].Name).To(Equal(reverseZoneName), "RRset should have setOwnerReference to Zone") + Expect(createdResource.GetFinalizers()).To(ContainElement(FINALIZER_NAME), "RRset should contain the finalizer") + }) + }) }) diff --git a/mkdocs.yml b/mkdocs.yml index e9f913c..be9a50c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -57,3 +57,4 @@ nav: - Guides: - Zones: guides/zones.md - RRsets: guides/rrsets.md + - Warnings: guides/warnings.md