Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions api/v1/condition_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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"
)
1 change: 1 addition & 0 deletions api/v1/zone_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
3 changes: 3 additions & 0 deletions config/crd/bases/cloudflare-operator.io_zones.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
40 changes: 34 additions & 6 deletions internal/controller/zone_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand All @@ -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
}

Expand Down
46 changes: 46 additions & 0 deletions internal/controller/zone_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package controller

import (
"context"
"errors"
"os"
"testing"

Expand Down Expand Up @@ -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)

Expand Down
Loading