diff --git a/api/v1/condition_types.go b/api/v1/condition_types.go index de6051a6..c240a2e7 100644 --- a/api/v1/condition_types.go +++ b/api/v1/condition_types.go @@ -20,6 +20,9 @@ const ( // ConditionTypeReady represents the fact that the object is ready. ConditionTypeReady string = "Ready" + // ConditionTypePruned represents the result of the latest DNS record pruning attempt. + ConditionTypePruned string = "Pruned" + // ConditionReasonReady represents the fact that the object is ready. ConditionReasonReady string = "Ready" @@ -28,4 +31,10 @@ const ( // ConditionReasonFailed represents the fact that the object has failed. ConditionReasonFailed string = "Failed" + + // ConditionReasonPruneSucceeded represents the fact that DNS record pruning succeeded. + ConditionReasonPruneSucceeded string = "PruneSucceeded" + + // ConditionReasonPruneFailed represents the fact that DNS record pruning failed. + ConditionReasonPruneFailed string = "PruneFailed" ) diff --git a/api/v1/zone_types.go b/api/v1/zone_types.go index b5de5422..8ae5bad8 100644 --- a/api/v1/zone_types.go +++ b/api/v1/zone_types.go @@ -68,6 +68,7 @@ const ( // +kubebuilder:printcolumn:name="Zone Name",type="string",JSONPath=".spec.name" // +kubebuilder:printcolumn:name="ID",type="string",JSONPath=".status.id" // +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=`.status.conditions[?(@.type == "Ready")].status` +// +kubebuilder:printcolumn:name="Pruned",type="string",JSONPath=`.status.conditions[?(@.type == "Pruned")].status` type Zone struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` diff --git a/config/crd/bases/cloudflare-operator.io_zones.yaml b/config/crd/bases/cloudflare-operator.io_zones.yaml index b96d06ca..369f3cd9 100644 --- a/config/crd/bases/cloudflare-operator.io_zones.yaml +++ b/config/crd/bases/cloudflare-operator.io_zones.yaml @@ -24,6 +24,9 @@ spec: - jsonPath: .status.conditions[?(@.type == "Ready")].status name: Ready type: string + - jsonPath: .status.conditions[?(@.type == "Pruned")].status + name: Pruned + type: string name: v1 schema: openAPIV3Schema: diff --git a/internal/controller/zone_controller.go b/internal/controller/zone_controller.go index c7dc813f..fe4990d9 100644 --- a/internal/controller/zone_controller.go +++ b/internal/controller/zone_controller.go @@ -26,8 +26,10 @@ import ( cloudflare "github.com/cloudflare/cloudflare-go/v7" "github.com/cloudflare/cloudflare-go/v7/dns" + "github.com/fluxcd/pkg/runtime/conditions" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" apierrutil "k8s.io/apimachinery/pkg/util/errors" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -152,18 +154,45 @@ func (r *ZoneReconciler) reconcileZone(ctx context.Context, zone *cloudflareoper zone.Status.ID = zoneID + var pruneErr error if zone.Spec.Prune { - if err := r.handlePrune(ctx, cloudflareAPI, zone); err != nil { - intconditions.MarkFalse(zone, fmt.Errorf("failed to prune DNS records: %v", err)) - return ctrl.Result{RequeueAfter: r.RetryInterval}, nil - } + pruneErr = r.handlePrune(ctx, cloudflareAPI, zone) } + setZoneReadyConditions(zone, pruneErr) - intconditions.MarkTrue(zone, "Zone is ready") + if pruneErr != nil { + return ctrl.Result{RequeueAfter: r.RetryInterval}, nil + } return ctrl.Result{RequeueAfter: zone.Spec.Interval.Duration}, nil } +func setZoneReadyConditions(zone *cloudflareoperatoriov1.Zone, pruneErr error) { + intconditions.MarkTrue(zone, "Zone is ready") + + if !zone.Spec.Prune { + conditions.Delete(zone, cloudflareoperatoriov1.ConditionTypePruned) + return + } + + if pruneErr != nil { + conditions.Set(zone, &metav1.Condition{ + Type: cloudflareoperatoriov1.ConditionTypePruned, + Status: metav1.ConditionFalse, + Reason: cloudflareoperatoriov1.ConditionReasonPruneFailed, + Message: fmt.Sprintf("failed to prune DNS records: %v", pruneErr), + }) + return + } + + conditions.Set(zone, &metav1.Condition{ + Type: cloudflareoperatoriov1.ConditionTypePruned, + Status: metav1.ConditionTrue, + Reason: cloudflareoperatoriov1.ConditionReasonPruneSucceeded, + Message: "DNS record pruning succeeded", + }) +} + // handlePrune deletes DNS records that are not managed by the operator if enabled func (r *ZoneReconciler) handlePrune(ctx context.Context, cloudflareAPI *cloudflare.Client, zone *cloudflareoperatoriov1.Zone) error { log := ctrl.LoggerFrom(ctx) @@ -182,7 +211,6 @@ func (r *ZoneReconciler) handlePrune(ctx context.Context, cloudflareAPI *cloudfl cloudflareDNSRecords, err := listCloudflareDNSRecords(ctx, cloudflareAPI, zone.Status.ID, dns.RecordListParams{}) if err != nil { - intconditions.MarkFalse(zone, err) return err } diff --git a/internal/controller/zone_controller_test.go b/internal/controller/zone_controller_test.go index 846dd6ad..816690d5 100644 --- a/internal/controller/zone_controller_test.go +++ b/internal/controller/zone_controller_test.go @@ -18,6 +18,7 @@ package controller import ( "context" + "errors" "os" "testing" @@ -164,6 +165,51 @@ func TestZoneReconciler_reconcileZone(t *testing.T) { }) } +func TestSetZoneReadyConditions(t *testing.T) { + t.Run("prune disabled", func(t *testing.T) { + g := NewWithT(t) + zone := &cloudflareoperatoriov1.Zone{} + conditions.Set(zone, conditions.FalseCondition( + cloudflareoperatoriov1.ConditionTypePruned, + cloudflareoperatoriov1.ConditionReasonPruneFailed, + "old prune failure", + )) + + setZoneReadyConditions(zone, nil) + + g.Expect(conditions.IsTrue(zone, cloudflareoperatoriov1.ConditionTypeReady)).To(BeTrue()) + g.Expect(conditions.Get(zone, cloudflareoperatoriov1.ConditionTypePruned)).To(BeNil()) + }) + + t.Run("prune succeeded", func(t *testing.T) { + g := NewWithT(t) + zone := &cloudflareoperatoriov1.Zone{ + Spec: cloudflareoperatoriov1.ZoneSpec{Prune: true}, + } + + setZoneReadyConditions(zone, nil) + + g.Expect(conditions.IsTrue(zone, cloudflareoperatoriov1.ConditionTypeReady)).To(BeTrue()) + g.Expect(conditions.IsTrue(zone, cloudflareoperatoriov1.ConditionTypePruned)).To(BeTrue()) + g.Expect(conditions.GetReason(zone, cloudflareoperatoriov1.ConditionTypePruned)).To(Equal(cloudflareoperatoriov1.ConditionReasonPruneSucceeded)) + g.Expect(conditions.GetMessage(zone, cloudflareoperatoriov1.ConditionTypePruned)).To(Equal("DNS record pruning succeeded")) + }) + + t.Run("prune failed", func(t *testing.T) { + g := NewWithT(t) + zone := &cloudflareoperatoriov1.Zone{ + Spec: cloudflareoperatoriov1.ZoneSpec{Prune: true}, + } + + setZoneReadyConditions(zone, errors.New("Cloudflare API unavailable")) + + g.Expect(conditions.IsTrue(zone, cloudflareoperatoriov1.ConditionTypeReady)).To(BeTrue()) + g.Expect(conditions.IsFalse(zone, cloudflareoperatoriov1.ConditionTypePruned)).To(BeTrue()) + g.Expect(conditions.GetReason(zone, cloudflareoperatoriov1.ConditionTypePruned)).To(Equal(cloudflareoperatoriov1.ConditionReasonPruneFailed)) + g.Expect(conditions.GetMessage(zone, cloudflareoperatoriov1.ConditionTypePruned)).To(Equal("failed to prune DNS records: Cloudflare API unavailable")) + }) +} + func TestManagedDNSRecordKeysForZone(t *testing.T) { g := NewWithT(t)