From 73f9739106c348bc040fb83fa13114bc064aa0c0 Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Tue, 13 Jan 2026 09:41:43 -0800 Subject: [PATCH 1/4] feat: tsigkey implementation --- Makefile | 9 +- cmd/main.go | 13 + .../dns.networking.miloapis.com_tsigkeys.yaml | 2 +- config/rbac/role.yaml | 4 + config/samples/dns_v1alpha1_tsigkey.yaml | 2 +- internal/controller/conditions.go | 1 + .../controller/tsigkey_powerdns_controller.go | 375 +++++++++++++ .../tsigkey_powerdns_controller_test.go | 226 ++++++++ .../tsigkey_replicator_controller.go | 352 +++++++++++++ internal/pdns/client.go | 139 +++++ internal/pdns/pdns_integration_test.go | 181 +++++++ internal/pdns/pdns_test.go | 101 ++++ test/e2e/kubeconfig-downstream | 19 + test/e2e/kubeconfig-upstream | 19 + test/e2e/tsig/chainsaw-test.yaml | 491 ++++++++++++++++++ .../chainsaw-test.yaml | 7 +- 16 files changed, 1934 insertions(+), 7 deletions(-) create mode 100644 internal/controller/tsigkey_powerdns_controller.go create mode 100644 internal/controller/tsigkey_powerdns_controller_test.go create mode 100644 internal/controller/tsigkey_replicator_controller.go create mode 100644 test/e2e/kubeconfig-downstream create mode 100644 test/e2e/kubeconfig-upstream create mode 100644 test/e2e/tsig/chainsaw-test.yaml rename test/e2e/{ => zones-and-records}/chainsaw-test.yaml (98%) diff --git a/Makefile b/Makefile index 2df25c9..ce6e312 100644 --- a/Makefile +++ b/Makefile @@ -412,10 +412,15 @@ chainsaw: ## Find or download chainsaw $(call go-install-tool,$(CHAINSAW),github.com/kyverno/chainsaw,$(CHAINSAW_VERSION)) .PHONY: chainsaw-test -chainsaw-test: chainsaw chainsaw-prepare-kubeconfigs ## Run Chainsaw tests (requires dev/kind.*.kubeconfig to exist) +CHAINSAW_TEST_DIR ?= . +CHAINSAW_DEFAULT_ARGS ?= +CHAINSAW_ARGS ?= + +.PHONY: chainsaw-test +chainsaw-test: chainsaw chainsaw-prepare-kubeconfigs ## Run Chainsaw tests (set CHAINSAW_TEST_DIR=./tsig and/or CHAINSAW_ARGS="--include-test-regex ") @test -f dev/kind.upstream.kubeconfig || { echo "Missing dev/kind.upstream.kubeconfig. Bootstrap clusters first."; exit 1; } @test -f dev/kind.downstream.kubeconfig || { echo "Missing dev/kind.downstream.kubeconfig. Bootstrap clusters first."; exit 1; } - cd test/e2e && $(CHAINSAW) test . + cd test/e2e && $(CHAINSAW) test $(CHAINSAW_DEFAULT_ARGS) $(CHAINSAW_ARGS) $(CHAINSAW_TEST_DIR) .PHONY: chainsaw-prepare-kubeconfigs chainsaw-prepare-kubeconfigs: ## Copy dev kind kubeconfigs into test/e2e for stable relative resolution diff --git a/cmd/main.go b/cmd/main.go index 895cb60..bdbd1ec 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -231,6 +231,13 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "DNSRecordSetPowerDNS") os.Exit(1) } + if err := (&controller.TSIGKeyPowerDNSReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "TSIGKeyPowerDNS") + os.Exit(1) + } if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { setupLog.Error(err, "unable to set up health check") @@ -306,6 +313,12 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "DNSZoneDiscoveryReplicator") os.Exit(1) } + if err := (&controller.TSIGKeyReplicator{ + DownstreamClient: downstreamCluster.GetClient(), + }).SetupWithManager(mcmgr, downstreamCluster); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "TSIGKeyReplicator") + os.Exit(1) + } if err := mcmgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { setupLog.Error(err, "unable to set up health check") diff --git a/config/crd/bases/dns.networking.miloapis.com_tsigkeys.yaml b/config/crd/bases/dns.networking.miloapis.com_tsigkeys.yaml index c7e5eb7..789504a 100644 --- a/config/crd/bases/dns.networking.miloapis.com_tsigkeys.yaml +++ b/config/crd/bases/dns.networking.miloapis.com_tsigkeys.yaml @@ -183,7 +183,7 @@ spec: type: string tsigKeyID: description: TSIGKeyID is the opaque provider identifier for the TSIG - key (e.g., PowerDNS TSIG key ID). + key. type: string type: object required: diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index f138a63..7cdd26a 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -8,6 +8,7 @@ rules: - "" resources: - configmaps + - secrets verbs: - create - delete @@ -44,6 +45,7 @@ rules: - dns.networking.miloapis.com resources: - dnsrecordsets/finalizers + - tsigkeys/finalizers verbs: - update - apiGroups: @@ -52,6 +54,7 @@ rules: - dnsrecordsets/status - dnszonediscoveries/status - dnszones/status + - tsigkeys/status verbs: - get - patch @@ -68,6 +71,7 @@ rules: - dns.networking.miloapis.com resources: - dnszonediscoveries + - tsigkeys verbs: - get - list diff --git a/config/samples/dns_v1alpha1_tsigkey.yaml b/config/samples/dns_v1alpha1_tsigkey.yaml index a049906..43bf1d8 100644 --- a/config/samples/dns_v1alpha1_tsigkey.yaml +++ b/config/samples/dns_v1alpha1_tsigkey.yaml @@ -10,6 +10,6 @@ spec: # algorithm: hmac-sha256 # Optional (BYO secret): # secretRef: - # name: existing-upstream-tsig + # name: existing-upstream diff --git a/internal/controller/conditions.go b/internal/controller/conditions.go index 637a57e..68daf1b 100644 --- a/internal/controller/conditions.go +++ b/internal/controller/conditions.go @@ -8,6 +8,7 @@ const ( ReasonAccepted = "Accepted" ReasonPending = "Pending" ReasonInvalidDNSRecordSet = "InvalidDNSRecordSet" + ReasonInvalidSecret = "InvalidSecret" ReasonProgrammed = "Programmed" ReasonDiscovered = "Discovered" ReasonDNSZoneInUse = "DNSZoneInUse" diff --git a/internal/controller/tsigkey_powerdns_controller.go b/internal/controller/tsigkey_powerdns_controller.go new file mode 100644 index 0000000..582c9b8 --- /dev/null +++ b/internal/controller/tsigkey_powerdns_controller.go @@ -0,0 +1,375 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +package controller + +import ( + "context" + "encoding/base64" + "fmt" + "time" + + dnsv1alpha1 "go.miloapis.com/dns-operator/api/v1alpha1" + pdnsclient "go.miloapis.com/dns-operator/internal/pdns" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/handler" + logf "sigs.k8s.io/controller-runtime/pkg/log" +) + +const tsigKeyPowerDNSFinalizer = "dns.networking.miloapis.com/finalize-tsigkey-powerdns" + +// TSIGKeyPDNS is the subset of the PowerDNS client used by the TSIGKey controller. +type TSIGKeyPDNS interface { + EnsureTSIGKey(ctx context.Context, name, algorithm, keyMaterial string) (pdnsclient.TSIGKey, error) + DeleteTSIGKey(ctx context.Context, id string) error + DeleteTSIGKeyByName(ctx context.Context, name string) error +} + +// TSIGKeyPowerDNSReconciler programs TSIG keys into PowerDNS. +type TSIGKeyPowerDNSReconciler struct { + client.Client + Scheme *runtime.Scheme + + // PDNS is optional; when nil, SetupWithManager constructs one from env via pdnsclient.NewFromEnv(). + PDNS TSIGKeyPDNS +} + +// +kubebuilder:rbac:groups=dns.networking.miloapis.com,resources=tsigkeys,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=dns.networking.miloapis.com,resources=tsigkeys/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=dns.networking.miloapis.com,resources=tsigkeys/finalizers,verbs=update +// +kubebuilder:rbac:groups=dns.networking.miloapis.com,resources=dnszones,verbs=get;list;watch +// +kubebuilder:rbac:groups=dns.networking.miloapis.com,resources=dnszoneclasses,verbs=get;list;watch +// +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch;create;update;patch;delete + +func (r *TSIGKeyPowerDNSReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := logf.FromContext(ctx).WithValues("namespace", req.Namespace, "name", req.Name) + logger.Info("tsigkey powerdns reconcile start") + + var tk dnsv1alpha1.TSIGKey + if err := r.Get(ctx, req.NamespacedName, &tk); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + // Deletion path: delete from PDNS, then drop finalizer. + if !tk.DeletionTimestamp.IsZero() { + if controllerutil.ContainsFinalizer(&tk, tsigKeyPowerDNSFinalizer) { + pdnsCli := r.PDNS + if pdnsCli == nil { + return ctrl.Result{}, fmt.Errorf("pdns client is nil (SetupWithManager not called?)") + } + // Best-effort cleanup by ID. + if tk.Status.TSIGKeyID != "" { + if err := pdnsCli.DeleteTSIGKey(ctx, tk.Status.TSIGKeyID); err != nil { + logger.Error(err, "failed to delete PDNS TSIG key by id; will retry", "id", tk.Status.TSIGKeyID) + return ctrl.Result{}, err + } + } else { + // If we don't have a provider ID, we can't safely delete an external key. + // This commonly means the key was never successfully programmed. + logger.Info("skipping PDNS TSIG key delete (missing status.tsigKeyID)") + } + + base := tk.DeepCopy() + controllerutil.RemoveFinalizer(&tk, tsigKeyPowerDNSFinalizer) + if err := r.Patch(ctx, &tk, client.MergeFrom(base)); err != nil { + return ctrl.Result{}, err + } + } + return ctrl.Result{}, nil + } + + // Ensure finalizer while active. + if !controllerutil.ContainsFinalizer(&tk, tsigKeyPowerDNSFinalizer) { + base := tk.DeepCopy() + controllerutil.AddFinalizer(&tk, tsigKeyPowerDNSFinalizer) + if err := r.Patch(ctx, &tk, client.MergeFrom(base)); err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{}, nil + } + + // Resolve zone + class and ensure this TSIGKey is for a PowerDNS zone. + zone, ok, err := r.resolveZone(ctx, &tk) + if err != nil { + return ctrl.Result{}, err + } + if !ok { + // dependency missing or wrong controller; status already updated + return ctrl.Result{}, nil + } + _, ok, err = r.resolveZoneClass(ctx, &tk, zone) + if err != nil { + return ctrl.Result{}, err + } + if !ok { + // dependency missing or wrong controller; status already updated + return ctrl.Result{}, nil + } + + // Ensure the DNSZone is an owner of this TSIGKey so GC cascades on zone deletion. + if !metav1.IsControlledBy(&tk, zone) { + base := tk.DeepCopy() + if err := controllerutil.SetControllerReference(zone, &tk, r.Scheme); err != nil { + return ctrl.Result{}, err + } + if err := r.Patch(ctx, &tk, client.MergeFrom(base)); err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{}, nil + } + + // Determine effective algorithm. + alg := tk.Spec.Algorithm + if alg == "" { + alg = dnsv1alpha1.TSIGAlgorithmHMACMD5 + } + + // Resolve key material from Secret (BYO) or generated secret. + secretName, keyMaterial, ok, err := r.resolveKeyMaterial(ctx, &tk, string(alg)) + if err != nil { + return ctrl.Result{}, err + } + if !ok { + // invalid secret schema etc; Accepted updated + return ctrl.Result{}, nil + } + + // Update status.secretName if needed. + if secretName != "" && tk.Status.SecretName != secretName { + base := tk.DeepCopy() + tk.Status.SecretName = secretName + if err := r.Status().Patch(ctx, &tk, client.MergeFrom(base)); err != nil { + return ctrl.Result{}, err + } + // re-fetch not required; continue + } + + if err := r.setAcceptedCondition(ctx, &tk, metav1.ConditionTrue, ReasonAccepted, "Accepted for zone"); err != nil { + return ctrl.Result{}, err + } + + created, pdnsErr := r.PDNS.EnsureTSIGKey(ctx, tk.Spec.KeyName, string(alg), keyMaterial) + if pdnsErr != nil { + _ = r.setProgrammedCondition(ctx, &tk, metav1.ConditionFalse, ReasonPDNSError, pdnsErr.Error()) + return ctrl.Result{}, pdnsErr + } + + // Persist provider ID. + if created.ID != "" && tk.Status.TSIGKeyID != created.ID { + base := tk.DeepCopy() + tk.Status.TSIGKeyID = created.ID + if err := r.Status().Patch(ctx, &tk, client.MergeFrom(base)); err != nil { + return ctrl.Result{}, err + } + } + + if err := r.setProgrammedCondition(ctx, &tk, metav1.ConditionTrue, ReasonProgrammed, "TSIG key programmed"); err != nil { + return ctrl.Result{}, err + } + + logger.Info("tsigkey powerdns reconcile complete") + return ctrl.Result{}, nil +} + +func (r *TSIGKeyPowerDNSReconciler) resolveZone(ctx context.Context, tk *dnsv1alpha1.TSIGKey) (*dnsv1alpha1.DNSZone, bool, error) { + // Zone lookup + var zone dnsv1alpha1.DNSZone + if err := r.Get(ctx, client.ObjectKey{Namespace: tk.Namespace, Name: tk.Spec.DNSZoneRef.Name}, &zone); err != nil { + if apierrors.IsNotFound(err) { + if err := r.setAcceptedCondition(ctx, tk, metav1.ConditionFalse, ReasonPending, + fmt.Sprintf("waiting for DNSZone %q", tk.Spec.DNSZoneRef.Name)); err != nil { + return nil, false, err + } + return nil, false, nil + } + return nil, false, err + } + if !zone.DeletionTimestamp.IsZero() { + if err := r.setAcceptedCondition(ctx, tk, metav1.ConditionFalse, ReasonPending, + fmt.Sprintf("DNSZone %q is deleting", zone.Name)); err != nil { + return nil, false, err + } + return nil, false, nil + } + return &zone, true, nil +} + +func (r *TSIGKeyPowerDNSReconciler) resolveZoneClass(ctx context.Context, tk *dnsv1alpha1.TSIGKey, zone *dnsv1alpha1.DNSZone) (*dnsv1alpha1.DNSZoneClass, bool, error) { + if zone.Spec.DNSZoneClassName == "" { + if err := r.setAcceptedCondition(ctx, tk, metav1.ConditionFalse, ReasonPending, + fmt.Sprintf("DNSZone %q has no class yet", zone.Name)); err != nil { + return nil, false, err + } + return nil, false, nil + } + + // Class lookup + var zc dnsv1alpha1.DNSZoneClass + if err := r.Get(ctx, client.ObjectKey{Name: zone.Spec.DNSZoneClassName}, &zc); err != nil { + if apierrors.IsNotFound(err) { + if err := r.setAcceptedCondition(ctx, tk, metav1.ConditionFalse, ReasonPending, + fmt.Sprintf("DNSZoneClass %q not found", zone.Spec.DNSZoneClassName)); err != nil { + return nil, false, err + } + return nil, false, nil + } + return nil, false, err + } + if zc.Spec.ControllerName != ControllerNamePowerDNS { + if err := r.setAcceptedCondition(ctx, tk, metav1.ConditionFalse, ReasonPending, + fmt.Sprintf("DNSZoneClass controller %q is not %q", zc.Spec.ControllerName, ControllerNamePowerDNS)); err != nil { + return nil, false, err + } + return nil, false, nil + } + return &zc, true, nil +} + +func (r *TSIGKeyPowerDNSReconciler) resolveKeyMaterial(ctx context.Context, tk *dnsv1alpha1.TSIGKey, algorithm string) (secretName string, keyMaterial string, ok bool, err error) { + // BYO secret: validate schema and do not mutate. + if tk.Spec.SecretRef != nil && tk.Spec.SecretRef.Name != "" { + var s corev1.Secret + if err := r.Get(ctx, client.ObjectKey{Namespace: tk.Namespace, Name: tk.Spec.SecretRef.Name}, &s); err != nil { + if apierrors.IsNotFound(err) { + _ = r.setAcceptedCondition(ctx, tk, metav1.ConditionFalse, ReasonPending, fmt.Sprintf("waiting for Secret %q", tk.Spec.SecretRef.Name)) + return tk.Spec.SecretRef.Name, "", false, nil + } + return "", "", false, err + } + secB := s.Data["secret"] + if len(secB) == 0 { + _ = r.setAcceptedCondition(ctx, tk, metav1.ConditionFalse, ReasonInvalidSecret, "secret.data.secret must be non-empty") + return s.Name, "", false, nil + } + // PDNS expects base64 key material. Secret.data.secret is already stored base64-encoded + // by Kubernetes at the API layer, so the bytes we get here are the original raw secret bytes. + return s.Name, base64.StdEncoding.EncodeToString(secB), true, nil + } + + // Generated secret mode: the replicator is responsible for creating and replicating this secret. + // The downstream controller only consumes and validates it. + secretName = tk.Name + var secret corev1.Secret + if err := r.Get(ctx, client.ObjectKey{Namespace: tk.Namespace, Name: secretName}, &secret); err != nil { + if apierrors.IsNotFound(err) { + _ = r.setAcceptedCondition(ctx, tk, metav1.ConditionFalse, ReasonPending, fmt.Sprintf("waiting for Secret %q", secretName)) + return secretName, "", false, nil + } + return "", "", false, err + } + secB := secret.Data["secret"] + if len(secB) == 0 { + _ = r.setAcceptedCondition(ctx, tk, metav1.ConditionFalse, ReasonInvalidSecret, "secret.data.secret must be non-empty") + return secretName, "", false, nil + } + return secretName, base64.StdEncoding.EncodeToString(secB), true, nil +} + +func (r *TSIGKeyPowerDNSReconciler) setAcceptedCondition(ctx context.Context, tk *dnsv1alpha1.TSIGKey, status metav1.ConditionStatus, reason, message string) error { + base := tk.DeepCopy() + cond := metav1.Condition{ + Type: CondAccepted, + Status: status, + Reason: reason, + Message: message, + ObservedGeneration: tk.Generation, + LastTransitionTime: metav1.NewTime(time.Now()), + } + if !apimeta.SetStatusCondition(&tk.Status.Conditions, cond) { + return nil + } + return r.Status().Patch(ctx, tk, client.MergeFrom(base)) +} + +func (r *TSIGKeyPowerDNSReconciler) setProgrammedCondition(ctx context.Context, tk *dnsv1alpha1.TSIGKey, status metav1.ConditionStatus, reason, message string) error { + base := tk.DeepCopy() + cond := metav1.Condition{ + Type: CondProgrammed, + Status: status, + Reason: reason, + Message: message, + ObservedGeneration: tk.Generation, + LastTransitionTime: metav1.NewTime(time.Now()), + } + if !apimeta.SetStatusCondition(&tk.Status.Conditions, cond) { + return nil + } + return r.Status().Patch(ctx, tk, client.MergeFrom(base)) +} + +func (r *TSIGKeyPowerDNSReconciler) SetupWithManager(mgr ctrl.Manager) error { + // Initialize PDNS client once at setup-time (unless injected, e.g. tests). + // This fails fast on bad env/config rather than failing on the first reconcile. + if r.PDNS == nil { + cli, err := pdnsclient.NewFromEnv() + if err != nil { + return fmt.Errorf("pdns client: %w", err) + } + r.PDNS = cli + } + + // index TSIGKey by dnsZoneRef for quick fan-out from a DNSZone event + if err := mgr.GetFieldIndexer().IndexField(context.Background(), + &dnsv1alpha1.TSIGKey{}, "spec.DNSZoneRef.Name", + func(obj client.Object) []string { + tk := obj.(*dnsv1alpha1.TSIGKey) + return []string{tk.Spec.DNSZoneRef.Name} + }, + ); err != nil { + return err + } + + // Index TSIGKey by the effective secret name stored in status.secretName. + // This is what the controller actually consumes (BYO secretRef or generated). + if err := mgr.GetFieldIndexer().IndexField(context.Background(), + &dnsv1alpha1.TSIGKey{}, "status.secretName", + func(obj client.Object) []string { + tk := obj.(*dnsv1alpha1.TSIGKey) + if tk.Status.SecretName != "" { + return []string{tk.Status.SecretName} + } + return nil + }, + ); err != nil { + return err + } + + return ctrl.NewControllerManagedBy(mgr). + For(&dnsv1alpha1.TSIGKey{}). + // When a DNSZone changes, enqueue its TSIGKeys. + Watches( + &dnsv1alpha1.DNSZone{}, + handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []ctrl.Request { + zone := obj.(*dnsv1alpha1.DNSZone) + var tks dnsv1alpha1.TSIGKeyList + _ = mgr.GetClient().List(ctx, &tks, client.InNamespace(zone.Namespace), client.MatchingFields{"spec.DNSZoneRef.Name": zone.Name}) + out := make([]ctrl.Request, 0, len(tks.Items)) + for i := range tks.Items { + out = append(out, ctrl.Request{NamespacedName: client.ObjectKeyFromObject(&tks.Items[i])}) + } + return out + }), + ). + // When a BYO secret changes, enqueue the TSIGKeys referencing it. + Watches( + &corev1.Secret{}, + handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []ctrl.Request { + sec := obj.(*corev1.Secret) + var tks dnsv1alpha1.TSIGKeyList + _ = mgr.GetClient().List(ctx, &tks, client.InNamespace(sec.Namespace), client.MatchingFields{"status.secretName": sec.Name}) + out := make([]ctrl.Request, 0, len(tks.Items)) + for i := range tks.Items { + out = append(out, ctrl.Request{NamespacedName: client.ObjectKeyFromObject(&tks.Items[i])}) + } + return out + }), + ). + Named("tsigkey-powerdns"). + Complete(r) +} diff --git a/internal/controller/tsigkey_powerdns_controller_test.go b/internal/controller/tsigkey_powerdns_controller_test.go new file mode 100644 index 0000000..58b6b46 --- /dev/null +++ b/internal/controller/tsigkey_powerdns_controller_test.go @@ -0,0 +1,226 @@ +package controller_test + +import ( + "context" + "testing" + + dnsv1alpha1 "go.miloapis.com/dns-operator/api/v1alpha1" + "go.miloapis.com/dns-operator/internal/controller" + pdnsclient "go.miloapis.com/dns-operator/internal/pdns" + corev1 "k8s.io/api/core/v1" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +type fakeTSIGPDNS struct { + ensureCalls []struct { + Name string + Algorithm string + Key string + } + deleteByIDCalls []string + deleteByNameCalls []string + + ensureResp pdnsclient.TSIGKey + ensureErr error +} + +func (f *fakeTSIGPDNS) EnsureTSIGKey(_ context.Context, name, algorithm, keyMaterial string) (pdnsclient.TSIGKey, error) { + f.ensureCalls = append(f.ensureCalls, struct { + Name string + Algorithm string + Key string + }{name, algorithm, keyMaterial}) + return f.ensureResp, f.ensureErr +} +func (f *fakeTSIGPDNS) DeleteTSIGKey(_ context.Context, id string) error { + f.deleteByIDCalls = append(f.deleteByIDCalls, id) + return nil +} +func (f *fakeTSIGPDNS) DeleteTSIGKeyByName(_ context.Context, name string) error { + f.deleteByNameCalls = append(f.deleteByNameCalls, name) + return nil +} + +func newDNSOnlyScheme(t *testing.T) *runtime.Scheme { + t.Helper() + s := runtime.NewScheme() + if err := dnsv1alpha1.AddToScheme(s); err != nil { + t.Fatalf("add dns scheme: %v", err) + } + if err := corev1.AddToScheme(s); err != nil { + t.Fatalf("add core scheme: %v", err) + } + return s +} + +func TestTSIGKeyPowerDNS_ByoSecret_ValidatesAndPrograms(t *testing.T) { + t.Parallel() + + scheme := newDNSOnlyScheme(t) + zone, zc := newZoneAndClass("example-com") + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "byo", Namespace: ns}, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "secret": []byte("supersecret"), + }, + } + + tk := &dnsv1alpha1.TSIGKey{ + ObjectMeta: metav1.ObjectMeta{Name: "xfr", Namespace: ns}, + Spec: dnsv1alpha1.TSIGKeySpec{ + DNSZoneRef: corev1.LocalObjectReference{Name: zone.Name}, + KeyName: "datum-example-com-xfr", + Algorithm: dnsv1alpha1.TSIGAlgorithmHMACSHA256, + SecretRef: &corev1.LocalObjectReference{Name: secret.Name}, + }, + } + + pdns := &fakeTSIGPDNS{ensureResp: pdnsclient.TSIGKey{ID: "pdns-id", Name: tk.Spec.KeyName, Algorithm: "hmac-sha256"}} + c := fake.NewClientBuilder(). + WithScheme(scheme). + WithStatusSubresource(&dnsv1alpha1.TSIGKey{}). + WithObjects(zone, zc, secret, tk). + Build() + + r := &controller.TSIGKeyPowerDNSReconciler{Client: c, Scheme: scheme, PDNS: pdns} + // Reconcile is multi-step (finalizer, ownerrefs, etc). Run a few times to converge. + for i := 0; i < 5; i++ { + _, err := r.Reconcile(context.Background(), ctrl.Request{NamespacedName: client.ObjectKeyFromObject(tk)}) + if err != nil { + t.Fatalf("reconcile error: %v", err) + } + } + + var got dnsv1alpha1.TSIGKey + if err := c.Get(context.Background(), client.ObjectKeyFromObject(tk), &got); err != nil { + t.Fatalf("get: %v", err) + } + + if cond := apimeta.FindStatusCondition(got.Status.Conditions, controller.CondAccepted); cond == nil || cond.Status != metav1.ConditionTrue { + t.Fatalf("Accepted not true: %#v", got.Status.Conditions) + } + if cond := apimeta.FindStatusCondition(got.Status.Conditions, controller.CondProgrammed); cond == nil || cond.Status != metav1.ConditionTrue { + t.Fatalf("Programmed not true: %#v", got.Status.Conditions) + } + if got.Status.TSIGKeyID != "pdns-id" { + t.Fatalf("expected tsigKeyID=pdns-id, got %q", got.Status.TSIGKeyID) + } + if got.Status.SecretName != secret.Name { + t.Fatalf("expected secretName=%q, got %q", secret.Name, got.Status.SecretName) + } + if len(pdns.ensureCalls) < 1 { + t.Fatalf("expected at least 1 ensure call, got %d", len(pdns.ensureCalls)) + } +} + +func TestTSIGKeyPowerDNS_ByoSecret_InvalidSchemaRejected(t *testing.T) { + t.Parallel() + + scheme := newDNSOnlyScheme(t) + zone, zc := newZoneAndClass("example-com") + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "byo", Namespace: ns}, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + // missing required secret key + }, + } + + tk := &dnsv1alpha1.TSIGKey{ + ObjectMeta: metav1.ObjectMeta{Name: "xfr", Namespace: ns}, + Spec: dnsv1alpha1.TSIGKeySpec{ + DNSZoneRef: corev1.LocalObjectReference{Name: zone.Name}, + KeyName: "datum-example-com-xfr", + Algorithm: dnsv1alpha1.TSIGAlgorithmHMACSHA256, + SecretRef: &corev1.LocalObjectReference{Name: secret.Name}, + }, + } + + pdns := &fakeTSIGPDNS{ensureResp: pdnsclient.TSIGKey{ID: "pdns-id"}} + c := fake.NewClientBuilder(). + WithScheme(scheme). + WithStatusSubresource(&dnsv1alpha1.TSIGKey{}). + WithObjects(zone, zc, secret, tk). + Build() + + r := &controller.TSIGKeyPowerDNSReconciler{Client: c, Scheme: scheme, PDNS: pdns} + for i := 0; i < 5; i++ { + _, err := r.Reconcile(context.Background(), ctrl.Request{NamespacedName: client.ObjectKeyFromObject(tk)}) + if err != nil { + t.Fatalf("reconcile error: %v", err) + } + } + + var got dnsv1alpha1.TSIGKey + _ = c.Get(context.Background(), client.ObjectKeyFromObject(tk), &got) + cond := apimeta.FindStatusCondition(got.Status.Conditions, controller.CondAccepted) + if cond == nil || cond.Status != metav1.ConditionFalse || cond.Reason != controller.ReasonInvalidSecret { + t.Fatalf("expected Accepted=False InvalidSecret, got %#v", got.Status.Conditions) + } + if len(pdns.ensureCalls) != 0 { + t.Fatalf("expected no PDNS ensure calls when secret invalid") + } +} + +func TestTSIGKeyPowerDNS_GeneratesSecretAndPrograms(t *testing.T) { + t.Parallel() + + scheme := newDNSOnlyScheme(t) + zone, zc := newZoneAndClass("example-com") + + tk := &dnsv1alpha1.TSIGKey{ + ObjectMeta: metav1.ObjectMeta{Name: "xfr", Namespace: ns}, + Spec: dnsv1alpha1.TSIGKeySpec{ + DNSZoneRef: corev1.LocalObjectReference{Name: zone.Name}, + KeyName: "datum-example-com-xfr", + Algorithm: dnsv1alpha1.TSIGAlgorithmHMACSHA256, + // SecretRef omitted => generated + }, + } + + // In generated-secret mode, the replicator is responsible for creating the Secret. + secretName := tk.Name + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: secretName, Namespace: ns}, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{ + "secret": []byte("supersecret"), + }, + } + + pdns := &fakeTSIGPDNS{ensureResp: pdnsclient.TSIGKey{ID: "pdns-id", Name: tk.Spec.KeyName, Algorithm: "hmac-sha256"}} + c := fake.NewClientBuilder(). + WithScheme(scheme). + WithStatusSubresource(&dnsv1alpha1.TSIGKey{}). + WithObjects(zone, zc, tk, secret). + Build() + + r := &controller.TSIGKeyPowerDNSReconciler{Client: c, Scheme: scheme, PDNS: pdns} + for i := 0; i < 5; i++ { + _, err := r.Reconcile(context.Background(), ctrl.Request{NamespacedName: client.ObjectKeyFromObject(tk)}) + if err != nil { + t.Fatalf("reconcile error: %v", err) + } + } + + var got dnsv1alpha1.TSIGKey + if err := c.Get(context.Background(), client.ObjectKeyFromObject(tk), &got); err != nil { + t.Fatalf("get: %v", err) + } + if got.Status.SecretName != secretName { + t.Fatalf("expected secretName=%q, got %q", secretName, got.Status.SecretName) + } + if len(pdns.ensureCalls) < 1 { + t.Fatalf("expected PDNS ensure called") + } +} + + diff --git a/internal/controller/tsigkey_replicator_controller.go b/internal/controller/tsigkey_replicator_controller.go new file mode 100644 index 0000000..8f44218 --- /dev/null +++ b/internal/controller/tsigkey_replicator_controller.go @@ -0,0 +1,352 @@ +// SPDX-License-Identifier: AGPL-3.0-only + +package controller + +import ( + "context" + "crypto/rand" + "fmt" + "strings" + + "k8s.io/apimachinery/pkg/api/equality" + apierrors "k8s.io/apimachinery/pkg/api/errors" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/cluster" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/multicluster-runtime/pkg/builder" + mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" + mcreconcile "sigs.k8s.io/multicluster-runtime/pkg/reconcile" + mcsource "sigs.k8s.io/multicluster-runtime/pkg/source" + + dnsv1alpha1 "go.miloapis.com/dns-operator/api/v1alpha1" + "go.miloapis.com/dns-operator/internal/downstreamclient" + corev1 "k8s.io/api/core/v1" +) + +const tsigKeyFinalizer = "dns.networking.miloapis.com/finalize-tsigkey" + +// TSIGKeyReplicator mirrors TSIGKey resources into the downstream cluster and reflects downstream status back upstream. +type TSIGKeyReplicator struct { + DownstreamClient client.Client + + mgr mcmanager.Manager +} + +// +kubebuilder:rbac:groups=dns.networking.miloapis.com,resources=tsigkeys,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=dns.networking.miloapis.com,resources=tsigkeys/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=dns.networking.miloapis.com,resources=tsigkeys/finalizers,verbs=update +// +kubebuilder:rbac:groups=dns.networking.miloapis.com,resources=dnszones,verbs=get;list;watch + +func (r *TSIGKeyReplicator) Reconcile(ctx context.Context, req mcreconcile.Request) (ctrl.Result, error) { + lg := log.FromContext(ctx).WithValues("cluster", req.ClusterName, "namespace", req.Namespace, "name", req.Name) + ctx = log.IntoContext(ctx, lg) + lg.Info("reconcile start") + + upstreamCluster, err := r.mgr.GetCluster(ctx, req.ClusterName) + if err != nil { + return ctrl.Result{}, err + } + + var upstream dnsv1alpha1.TSIGKey + if err := upstreamCluster.GetClient().Get(ctx, req.NamespacedName, &upstream); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + strategy := downstreamclient.NewMappedNamespaceResourceStrategy(req.ClusterName, upstreamCluster.GetClient(), r.DownstreamClient) + + // Ensure upstream finalizer (non-deletion path) + if upstream.DeletionTimestamp.IsZero() && !controllerutil.ContainsFinalizer(&upstream, tsigKeyFinalizer) { + base := upstream.DeepCopy() + controllerutil.AddFinalizer(&upstream, tsigKeyFinalizer) + if err := upstreamCluster.GetClient().Patch(ctx, &upstream, client.MergeFrom(base)); err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{}, nil + } + + // Deletion path: delete downstream shadow first then remove finalizer. + if !upstream.DeletionTimestamp.IsZero() { + done, err := r.handleDeletion(ctx, upstreamCluster.GetClient(), strategy, &upstream) + if err != nil { + return ctrl.Result{}, err + } + if !done { + return ctrl.Result{}, nil + } + return ctrl.Result{}, nil + } + + // Gate on referenced DNSZone early and update status when missing + var zone dnsv1alpha1.DNSZone + if err := upstreamCluster.GetClient().Get(ctx, types.NamespacedName{Namespace: req.Namespace, Name: upstream.Spec.DNSZoneRef.Name}, &zone); err != nil { + if apierrors.IsNotFound(err) { + zoneMsg := fmt.Sprintf("DNSZone %q not found", upstream.Spec.DNSZoneRef.Name) + if apimeta.SetStatusCondition(&upstream.Status.Conditions, metav1.Condition{ + Type: CondAccepted, + Status: metav1.ConditionFalse, + Reason: ReasonPending, + Message: zoneMsg, + ObservedGeneration: upstream.Generation, + LastTransitionTime: metav1.Now(), + }) { + base := upstream.DeepCopy() + if err := upstreamCluster.GetClient().Status().Patch(ctx, &upstream, client.MergeFrom(base)); err != nil { + return ctrl.Result{}, err + } + } + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } + + // If the zone is being deleted, do not program downstream key + if !zone.DeletionTimestamp.IsZero() { + return ctrl.Result{}, nil + } + + // Ensure OwnerReference to upstream DNSZone (same ns) + if !metav1.IsControlledBy(&upstream, &zone) { + base := upstream.DeepCopy() + if err := controllerutil.SetControllerReference(&zone, &upstream, upstreamCluster.GetScheme()); err != nil { + return ctrl.Result{}, err + } + if err := upstreamCluster.GetClient().Patch(ctx, &upstream, client.MergeFrom(base)); err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{}, nil + } + + // Ensure downstream shadow TSIGKey mirrors upstream spec. + if _, err := r.ensureDownstreamTSIGKey(ctx, req.ClusterName, strategy, &upstream); err != nil { + return ctrl.Result{}, err + } + + // Ensure Secret is present upstream and replicated to downstream so PowerDNS can consume it. + if err := r.ensureSecretReplication(ctx, req.ClusterName, upstreamCluster.GetClient(), strategy, &upstream); err != nil { + return ctrl.Result{}, err + } + + // Mirror downstream status when the shadow exists. + md, mdErr := strategy.ObjectMetaFromUpstreamObject(ctx, &upstream) + if mdErr != nil { + return ctrl.Result{}, mdErr + } + var shadow dnsv1alpha1.TSIGKey + if err := r.DownstreamClient.Get(ctx, types.NamespacedName{Namespace: md.Namespace, Name: md.Name}, &shadow); err != nil { + return ctrl.Result{}, err + } + if err := r.updateStatus(ctx, upstreamCluster.GetClient(), &upstream, shadow.Status.DeepCopy()); err != nil { + if !apierrors.IsNotFound(err) { + return ctrl.Result{}, err + } + } + return ctrl.Result{}, nil +} + +func (r *TSIGKeyReplicator) handleDeletion(ctx context.Context, c client.Client, strategy downstreamclient.ResourceStrategy, upstream *dnsv1alpha1.TSIGKey) (done bool, err error) { + if !controllerutil.ContainsFinalizer(upstream, tsigKeyFinalizer) { + return true, nil + } + + md, err := strategy.ObjectMetaFromUpstreamObject(ctx, upstream) + if err != nil { + return false, err + } + var shadow dnsv1alpha1.TSIGKey + shadow.SetNamespace(md.Namespace) + shadow.SetName(md.Name) + if err := r.DownstreamClient.Delete(ctx, &shadow); err != nil && !apierrors.IsNotFound(err) { + return false, err + } + + // Ensure it's gone before removing finalizer. + if err := r.DownstreamClient.Get(ctx, types.NamespacedName{Namespace: md.Namespace, Name: md.Name}, &shadow); err == nil { + return false, nil + } else if !apierrors.IsNotFound(err) { + return false, err + } + + base := upstream.DeepCopy() + controllerutil.RemoveFinalizer(upstream, tsigKeyFinalizer) + if err := c.Patch(ctx, upstream, client.MergeFrom(base)); err != nil { + return false, err + } + return true, nil +} + +func (r *TSIGKeyReplicator) ensureDownstreamTSIGKey(ctx context.Context, upstreamClusterName string, strategy downstreamclient.ResourceStrategy, upstream *dnsv1alpha1.TSIGKey) (controllerutil.OperationResult, error) { + md, err := strategy.ObjectMetaFromUpstreamObject(ctx, upstream) + if err != nil { + return controllerutil.OperationResultNone, err + } + + shadow := dnsv1alpha1.TSIGKey{} + shadow.SetNamespace(md.Namespace) + shadow.SetName(md.Name) + + // Ensure we create in the correct mapped namespace (and that it exists) by using the strategy client. + dsClient := strategy.GetClient() + + res, cErr := controllerutil.CreateOrPatch(ctx, dsClient, &shadow, func() error { + shadow.Labels = md.Labels + + if !equality.Semantic.DeepEqual(shadow.Spec, upstream.Spec) { + shadow.Spec = upstream.Spec + } + + // Set owner reference using the mapped-namespace strategy (anchor-based). + // NOTE: We intentionally do not manage anchor deletion here. + return strategy.SetControllerReference(ctx, upstream, &shadow) + }) + if cErr != nil { + return res, cErr + } + log.FromContext(ctx).Info("ensured downstream TSIGKey", "operation", res, "namespace", shadow.Namespace, "name", shadow.Name) + return res, nil +} + +func (r *TSIGKeyReplicator) updateStatus(ctx context.Context, c client.Client, upstream *dnsv1alpha1.TSIGKey, downstreamStatus *dnsv1alpha1.TSIGKeyStatus) error { + if downstreamStatus == nil { + return nil + } + if equality.Semantic.DeepEqual(upstream.Status, *downstreamStatus) { + return nil + } + base := upstream.DeepCopy() + upstream.Status = *downstreamStatus + return c.Status().Patch(ctx, upstream, client.MergeFrom(base)) +} + +func (r *TSIGKeyReplicator) ensureSecretReplication(ctx context.Context, upstreamClusterName string, upstreamClient client.Client, strategy downstreamclient.ResourceStrategy, upstream *dnsv1alpha1.TSIGKey) error { + // Determine the source secret name. + secretName := upstream.Name + if upstream.Spec.SecretRef != nil && upstream.Spec.SecretRef.Name != "" { + secretName = upstream.Spec.SecretRef.Name + } + + // Determine algorithm (default if omitted). + alg := upstream.Spec.Algorithm + if alg == "" { + alg = dnsv1alpha1.TSIGAlgorithmHMACMD5 + } + + // Ensure upstream secret exists (create only in generated-secret mode). + var src corev1.Secret + if err := upstreamClient.Get(ctx, client.ObjectKey{Namespace: upstream.Namespace, Name: secretName}, &src); err != nil { + if apierrors.IsNotFound(err) { + if upstream.Spec.SecretRef != nil && upstream.Spec.SecretRef.Name != "" { + // BYO secret not found yet; wait. + return nil + } + + // Generated mode: create the secret upstream. + src = corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: secretName, Namespace: upstream.Namespace}, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{}, + } + _, err := controllerutil.CreateOrPatch(ctx, upstreamClient, &src, func() error { + if src.Type == "" { + src.Type = corev1.SecretTypeOpaque + } + if src.Data == nil { + src.Data = map[string][]byte{} + } + if len(src.Data["secret"]) == 0 { + raw := make([]byte, 32) + if _, err := rand.Read(raw); err != nil { + return err + } + // Store raw secret bytes. (PowerDNS expects base64, but that is derived at reconciliation time.) + src.Data["secret"] = raw + } + return controllerutil.SetControllerReference(upstream, &src, upstreamClient.Scheme()) + }) + return err + } + return err + } + + // Ensure downstream secret mirrors upstream secret data. + md, err := strategy.ObjectMetaFromUpstreamObject(ctx, upstream) + if err != nil { + return err + } + dsClient := strategy.GetClient() // ensures downstream namespace exists on Create + + // Fetch downstream TSIGKey shadow for owner reference (GC in downstream). + var shadow dnsv1alpha1.TSIGKey + if err := r.DownstreamClient.Get(ctx, types.NamespacedName{Namespace: md.Namespace, Name: upstream.Name}, &shadow); err != nil { + return err + } + + dst := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Namespace: md.Namespace, Name: secretName}} + _, err = controllerutil.CreateOrPatch(ctx, dsClient, dst, func() error { + if dst.Type == "" { + dst.Type = corev1.SecretTypeOpaque + } + if dst.Data == nil { + dst.Data = map[string][]byte{} + } + // Copy secret bytes exactly. + dst.Data["secret"] = append([]byte(nil), src.Data["secret"]...) + + // GC: owned by downstream TSIGKey shadow. + if err := controllerutil.SetControllerReference(&shadow, dst, dsClient.Scheme()); err != nil { + // If the secret is already controlled by something else, leave it and error to surface issue. + return err + } + + // Stamp upstream-owner labels WITHOUT adding the anchor ConfigMap ownerRef. + // The Secret must be GC'd when the downstream TSIGKey shadow is deleted. + labels := dst.GetLabels() + if labels == nil { + labels = map[string]string{} + } + labels[downstreamclient.UpstreamOwnerClusterNameLabel] = fmt.Sprintf("cluster-%s", strings.ReplaceAll(upstreamClusterName, "/", "_")) + labels[downstreamclient.UpstreamOwnerGroupLabel] = dnsv1alpha1.GroupVersion.Group + labels[downstreamclient.UpstreamOwnerKindLabel] = "TSIGKey" + labels[downstreamclient.UpstreamOwnerNameLabel] = upstream.Name + labels[downstreamclient.UpstreamOwnerNamespaceLabel] = upstream.Namespace + dst.SetLabels(labels) + + return nil + }) + return err +} + +func (r *TSIGKeyReplicator) SetupWithManager(mgr mcmanager.Manager, downstreamCl cluster.Cluster) error { + r.mgr = mgr + + b := builder.ControllerManagedBy(mgr) + b = b.For(&dnsv1alpha1.TSIGKey{}) + + src := mcsource.TypedKind( + &dnsv1alpha1.TSIGKey{}, + downstreamclient.TypedEnqueueRequestForUpstreamOwner[*dnsv1alpha1.TSIGKey](&dnsv1alpha1.TSIGKey{}), + ) + clusterSrc, err := src.ForCluster("", downstreamCl) + if err != nil { + return fmt.Errorf("failed to build downstream watch for %s: %w", dnsv1alpha1.GroupVersion.WithKind("TSIGKey").String(), err) + } + b = b.WatchesRawSource(clusterSrc) + + // Also watch downstream Secrets (generated or BYO replicated) to ensure upstream TSIGKey reconcile + // happens when secret material changes (or is first created). + secretSrc := mcsource.TypedKind( + &corev1.Secret{}, + downstreamclient.TypedEnqueueRequestForUpstreamOwner[*corev1.Secret](&dnsv1alpha1.TSIGKey{}), + ) + secretClusterSrc, err := secretSrc.ForCluster("", downstreamCl) + if err != nil { + return fmt.Errorf("failed to build downstream watch for %s: %w", corev1.SchemeGroupVersion.WithKind("Secret").String(), err) + } + b = b.WatchesRawSource(secretClusterSrc) + + return b.Named("tsigkey-replicator").Complete(r) +} diff --git a/internal/pdns/client.go b/internal/pdns/client.go index b6acab0..b5a9cde 100644 --- a/internal/pdns/client.go +++ b/internal/pdns/client.go @@ -45,6 +45,16 @@ func NewClient(baseURL, apiKey string) *Client { } } +// TSIGKey represents a PowerDNS TSIGKey object. +// Note: the "key" field is typically empty when listing keys. +type TSIGKey struct { + Name string `json:"name"` + ID string `json:"id"` + Algorithm string `json:"algorithm"` + Key string `json:"key,omitempty"` + Type string `json:"type,omitempty"` +} + type pdnsAPIError struct { Status int Body string @@ -138,6 +148,135 @@ func (c *Client) GetZone(ctx context.Context, zone string) (string, error) { return zoneResponse.Name, nil } +// ListTSIGKeys lists all TSIG keys in the server. +func (c *Client) ListTSIGKeys(ctx context.Context) ([]TSIGKey, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.BaseURL+"/api/v1/servers/localhost/tsigkeys", nil) + if err != nil { + return nil, err + } + req.Header.Set("X-API-Key", c.APIKey) + resp, err := c.HTTP.Do(req) + if err != nil { + return nil, err + } + if resp.StatusCode/100 != 2 { + errBody := readRespBody(resp, 64<<10) // closes Body + return nil, &pdnsAPIError{Status: resp.StatusCode, Body: errBody} + } + defer func() { _ = resp.Body.Close() }() + var out []TSIGKey + if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { + return nil, err + } + return out, nil +} + +// CreateTSIGKey creates a TSIG key in PowerDNS. keyMaterial may be empty to let the server generate it. +func (c *Client) CreateTSIGKey(ctx context.Context, name, algorithm, keyMaterial string) (TSIGKey, error) { + payload := map[string]string{ + "name": name, + "algorithm": algorithm, + } + if keyMaterial != "" { + payload["key"] = keyMaterial + } + body, _ := json.Marshal(payload) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.BaseURL+"/api/v1/servers/localhost/tsigkeys", bytes.NewReader(body)) + if err != nil { + return TSIGKey{}, err + } + req.Header.Set("X-API-Key", c.APIKey) + req.Header.Set("Content-Type", "application/json") + resp, err := c.HTTP.Do(req) + if err != nil { + return TSIGKey{}, err + } + if resp.StatusCode/100 != 2 { + errBody := readRespBody(resp, 64<<10) // closes Body + return TSIGKey{}, &pdnsAPIError{Status: resp.StatusCode, Body: errBody} + } + defer func() { _ = resp.Body.Close() }() + var out TSIGKey + if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { + return TSIGKey{}, err + } + return out, nil +} + +// FindTSIGKeyByName finds a TSIG key by name, returning nil when not found. +func (c *Client) FindTSIGKeyByName(ctx context.Context, name string) (*TSIGKey, error) { + keys, err := c.ListTSIGKeys(ctx) + if err != nil { + return nil, err + } + for i := range keys { + if keys[i].Name == name { + k := keys[i] + return &k, nil + } + } + return nil, nil +} + +// EnsureTSIGKey ensures a TSIG key exists with the given name/algorithm. If a key exists +// with a different algorithm, it will be deleted and recreated. +func (c *Client) EnsureTSIGKey(ctx context.Context, name, algorithm, keyMaterial string) (TSIGKey, error) { + existing, err := c.FindTSIGKeyByName(ctx, name) + if err != nil { + return TSIGKey{}, err + } + if existing != nil { + if existing.Algorithm == algorithm { + return *existing, nil + } + // Recreate when algorithm differs. + if existing.ID != "" { + if err := c.DeleteTSIGKey(ctx, existing.ID); err != nil { + return TSIGKey{}, err + } + } + } + return c.CreateTSIGKey(ctx, name, algorithm, keyMaterial) +} + +// DeleteTSIGKey deletes a TSIG key by ID. +func (c *Client) DeleteTSIGKey(ctx context.Context, id string) error { + if id == "" { + return nil + } + req, err := http.NewRequestWithContext(ctx, http.MethodDelete, c.BaseURL+"/api/v1/servers/localhost/tsigkeys/"+id, nil) + if err != nil { + return err + } + req.Header.Set("X-API-Key", c.APIKey) + resp, err := c.HTTP.Do(req) + if err != nil { + return err + } + if resp.StatusCode == http.StatusNotFound { + _ = resp.Body.Close() + return nil + } + if resp.StatusCode/100 != 2 { + errBody := readRespBody(resp, 64<<10) // closes Body + return &pdnsAPIError{Status: resp.StatusCode, Body: errBody} + } + _ = resp.Body.Close() + return nil +} + +// DeleteTSIGKeyByName deletes a TSIG key by name (no-op if not found). +func (c *Client) DeleteTSIGKeyByName(ctx context.Context, name string) error { + existing, err := c.FindTSIGKeyByName(ctx, name) + if err != nil { + return err + } + if existing == nil { + return nil + } + return c.DeleteTSIGKey(ctx, existing.ID) +} + // in package pdns (same file as CreateZone/GetZone) func (c *Client) DeleteZone(ctx context.Context, zone string) error { req, err := http.NewRequestWithContext(ctx, http.MethodDelete, diff --git a/internal/pdns/pdns_integration_test.go b/internal/pdns/pdns_integration_test.go index c7b344f..6493e01 100644 --- a/internal/pdns/pdns_integration_test.go +++ b/internal/pdns/pdns_integration_test.go @@ -1,8 +1,13 @@ package pdns import ( + "bytes" "context" + "encoding/base64" + "encoding/json" + "errors" "fmt" + "net/http" "os" "path/filepath" "sort" @@ -428,3 +433,179 @@ func TestPDNS_ApplyRecordSetAuthoritative_CleansRemovedOwners(t *testing.T) { t.Fatalf("SOA rrset count changed: before=%d after=%d", soaBefore, got) } } + +func TestPDNS_TSIGKey_CreateWithSuppliedKey(t *testing.T) { + // No t.Parallel(): container + real PDNS. + const apiKey = "itest-key" + baseURL, stop := startPDNS(t, apiKey) + defer stop() + + client := NewClient(baseURL, apiKey) + ctx := context.Background() + + keyMaterial := base64.StdEncoding.EncodeToString([]byte("supersecret")) + created, err := client.CreateTSIGKey(ctx, "mytsigkey", "hmac-sha256", keyMaterial) + if err != nil { + t.Fatalf("CreateTSIGKey: %v", err) + } + if created.ID == "" { + t.Fatalf("expected non-empty id, got %#v", created) + } + if created.Name != "mytsigkey" { + t.Fatalf("expected name=mytsigkey, got %#v", created) + } + if created.Algorithm != "hmac-sha256" { + t.Fatalf("expected algorithm=hmac-sha256, got %#v", created) + } +} + +func TestPDNS_TSIGKey_CreateAndDeleteByID(t *testing.T) { + // No t.Parallel(): container + real PDNS. + const apiKey = "itest-key" + baseURL, stop := startPDNS(t, apiKey) + defer stop() + + client := NewClient(baseURL, apiKey) + ctx := context.Background() + + keyMaterial := base64.StdEncoding.EncodeToString([]byte("supersecret")) + created, err := client.CreateTSIGKey(ctx, "mytsigkey-delete", "hmac-sha256", keyMaterial) + if err != nil { + t.Fatalf("CreateTSIGKey: %v", err) + } + if created.ID == "" { + t.Fatalf("expected non-empty id: %#v", created) + } + + if err := client.DeleteTSIGKey(ctx, created.ID); err != nil { + t.Fatalf("DeleteTSIGKey: %v", err) + } + + found, err := client.FindTSIGKeyByName(ctx, "mytsigkey-delete") + if err != nil { + t.Fatalf("FindTSIGKeyByName: %v", err) + } + if found != nil { + t.Fatalf("expected key deleted, found %#v", found) + } +} + +func TestPDNS_TSIGKey_IDHasTrailingDot_AndDuplicateNameIsRejected(t *testing.T) { + // No t.Parallel(): container + real PDNS. + const apiKey = "itest-key" + baseURL, stop := startPDNS(t, apiKey) + defer stop() + + client := NewClient(baseURL, apiKey) + ctx := context.Background() + + // Use the same provider-visible name twice. + const name = "mytsigkey-duplicate" + + k1, err := client.CreateTSIGKey(ctx, name, "hmac-sha256", base64.StdEncoding.EncodeToString([]byte("supersecret-1"))) + if err != nil { + t.Fatalf("CreateTSIGKey #1: %v", err) + } + t.Cleanup(func() { _ = client.DeleteTSIGKey(context.Background(), k1.ID) }) + + if k1.Name != name { + t.Fatalf("expected name=%q, got %#v", name, k1) + } + if k1.ID == "" { + t.Fatalf("expected non-empty id, got %#v", k1) + } + if !strings.HasSuffix(k1.ID, ".") { + t.Fatalf("expected PDNS TSIGKey ID to have trailing dot, got %q", k1.ID) + } + + // PowerDNS enforces name uniqueness; creating a second key with the same name should be rejected. + _, err = client.CreateTSIGKey(ctx, name, "hmac-sha256", base64.StdEncoding.EncodeToString([]byte("supersecret-2"))) + if err == nil { + t.Fatalf("expected CreateTSIGKey #2 to fail with conflict, but got nil error") + } + var apiErr *pdnsAPIError + if !errors.As(err, &apiErr) || apiErr.Status != 409 { + t.Fatalf("expected 409 Conflict from CreateTSIGKey #2, got err=%T %v", err, err) + } +} + +func TestPDNS_TSIGKey_NameWithTrailingDot_IDHasSingleTrailingDot(t *testing.T) { + // No t.Parallel(): container + real PDNS. + const apiKey = "itest-key" + baseURL, stop := startPDNS(t, apiKey) + defer stop() + + client := NewClient(baseURL, apiKey) + ctx := context.Background() + + const nameWithDot = "mytsigkey-trailing-dot." + created, err := client.CreateTSIGKey(ctx, nameWithDot, "hmac-sha256", base64.StdEncoding.EncodeToString([]byte("supersecret"))) + if err != nil { + t.Fatalf("CreateTSIGKey: %v", err) + } + t.Cleanup(func() { _ = client.DeleteTSIGKey(context.Background(), created.ID) }) + + // PowerDNS normalizes TSIGKey.Name by stripping a trailing dot. + wantName := strings.TrimSuffix(nameWithDot, ".") + if created.Name != wantName { + t.Fatalf("expected created.Name=%q, got %#v", wantName, created) + } + + if created.ID == "" { + t.Fatalf("expected non-empty id, got %#v", created) + } + if !strings.HasSuffix(created.ID, ".") { + t.Fatalf("expected id to have trailing dot, got %q", created.ID) + } + if strings.HasSuffix(created.ID, "..") { + t.Fatalf("expected id to not end with two trailing dots, got %q", created.ID) + } + if created.ID != created.Name+"." { + t.Fatalf("expected id to equal name + trailing dot, got name=%q id=%q", created.Name, created.ID) + } +} + +func TestPDNS_TSIGKey_DuplicateNameEvenWithIDFieldIsRejected(t *testing.T) { + // No t.Parallel(): container + real PDNS. + const apiKey = "itest-key" + baseURL, stop := startPDNS(t, apiKey) + defer stop() + + client := NewClient(baseURL, apiKey) + ctx := context.Background() + + const name = "mytsigkey-dup-with-id-field" + + created, err := client.CreateTSIGKey(ctx, name, "hmac-sha256", base64.StdEncoding.EncodeToString([]byte("supersecret-1"))) + if err != nil { + t.Fatalf("CreateTSIGKey #1: %v", err) + } + t.Cleanup(func() { _ = client.DeleteTSIGKey(context.Background(), created.ID) }) + + // Try to create the same name again, but include an explicit ID field in the request body. + // PowerDNS should still reject duplicate names. + payload := map[string]string{ + "name": name, + "algorithm": "hmac-sha256", + "key": base64.StdEncoding.EncodeToString([]byte("supersecret-2")), + "id": "some-explicit-id-that-should-not-matter.", + } + body, _ := json.Marshal(payload) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, client.BaseURL+"/api/v1/servers/localhost/tsigkeys", bytes.NewReader(body)) + if err != nil { + t.Fatalf("NewRequest: %v", err) + } + req.Header.Set("X-API-Key", client.APIKey) + req.Header.Set("Content-Type", "application/json") + + resp, err := client.HTTP.Do(req) + if err != nil { + t.Fatalf("Do: %v", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusConflict { + t.Fatalf("expected 409 Conflict for duplicate name even with id field, got %d", resp.StatusCode) + } +} diff --git a/internal/pdns/pdns_test.go b/internal/pdns/pdns_test.go index b08c838..7da9558 100644 --- a/internal/pdns/pdns_test.go +++ b/internal/pdns/pdns_test.go @@ -532,6 +532,107 @@ func TestNewFromEnv(t *testing.T) { } } +func TestTSIGKey_CRUD(t *testing.T) { + t.Parallel() + + type state struct { + keys []TSIGKey + } + st := &state{ + keys: []TSIGKey{ + {Name: "existing", ID: "k1", Algorithm: "hmac-md5", Type: "TSIGKey"}, + }, + } + + mux := http.NewServeMux() + mux.HandleFunc("/api/v1/servers/localhost/tsigkeys", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + _ = json.NewEncoder(w).Encode(st.keys) + case http.MethodPost: + body, _ := io.ReadAll(r.Body) + var req map[string]string + _ = json.Unmarshal(body, &req) + created := TSIGKey{ + Name: req["name"], + Algorithm: req["algorithm"], + ID: "newid", + Type: "TSIGKey", + } + if k := req["key"]; k != "" { + created.Key = k + } + // Append to state so a subsequent list would show it. + st.keys = append(st.keys, created) + _ = json.NewEncoder(w).Encode(created) + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } + }) + mux.HandleFunc("/api/v1/servers/localhost/tsigkeys/", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodDelete { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + id := strings.TrimPrefix(r.URL.Path, "/api/v1/servers/localhost/tsigkeys/") + // Remove from state + out := st.keys[:0] + for _, k := range st.keys { + if k.ID == id { + continue + } + out = append(out, k) + } + st.keys = out + w.WriteHeader(http.StatusNoContent) + }) + + srv := httptest.NewServer(mux) + defer srv.Close() + + c := NewClient(srv.URL, "sekret") + ctx := context.Background() + + // list + keys, err := c.ListTSIGKeys(ctx) + if err != nil || len(keys) != 1 || keys[0].Name != "existing" { + t.Fatalf("ListTSIGKeys got=%v err=%v", keys, err) + } + + // find + found, err := c.FindTSIGKeyByName(ctx, "existing") + if err != nil || found == nil || found.ID != "k1" { + t.Fatalf("FindTSIGKeyByName got=%#v err=%v", found, err) + } + + // create with key + created, err := c.CreateTSIGKey(ctx, "mykey", "hmac-sha256", "b64secret") + if err != nil || created.ID == "" || created.Name != "mykey" || created.Algorithm != "hmac-sha256" { + t.Fatalf("CreateTSIGKey got=%#v err=%v", created, err) + } + + // ensure returns existing when matches + ens, err := c.EnsureTSIGKey(ctx, "existing", "hmac-md5", "ignored") + if err != nil || ens.ID != "k1" { + t.Fatalf("EnsureTSIGKey existing got=%#v err=%v", ens, err) + } + + // ensure recreates when algorithm differs (existing removed, new created) + ens2, err := c.EnsureTSIGKey(ctx, "existing", "hmac-sha256", "b64secret2") + if err != nil || ens2.ID != "newid" || ens2.Name != "existing" || ens2.Algorithm != "hmac-sha256" { + t.Fatalf("EnsureTSIGKey recreate got=%#v err=%v", ens2, err) + } + + // delete by name + if err := c.DeleteTSIGKeyByName(ctx, "existing"); err != nil { + t.Fatalf("DeleteTSIGKeyByName err=%v", err) + } + after, _ := c.FindTSIGKeyByName(ctx, "existing") + if after != nil { + t.Fatalf("expected existing deleted, got %#v", after) + } +} + func TestSOASerialAutoChangesPerDay(t *testing.T) { t.Parallel() diff --git a/test/e2e/kubeconfig-downstream b/test/e2e/kubeconfig-downstream new file mode 100644 index 0000000..6af829e --- /dev/null +++ b/test/e2e/kubeconfig-downstream @@ -0,0 +1,19 @@ +apiVersion: v1 +clusters: +- cluster: + certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCVENDQWUyZ0F3SUJBZ0lJSkVxdDlJUTRvNkl3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TmpBeE1USXlNRE0wTVRoYUZ3MHpOakF4TVRBeU1ETTVNVGhhTUJVeApFekFSQmdOVkJBTVRDbXQxWW1WeWJtVjBaWE13Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLCkFvSUJBUUNuWGpVQkR6cTRpenpiNlVUdndPSFpoN1hhaEJpZm5aZ0pXdDN2VUIreTUyZzRhOFovTVozSnlBZ2QKUlZJZk1iVEJFY2JtdllEb08wOTBjaXA3M3RHTFBFczJYSnMwZVFPVmNrVzRSUHlUb2g5djJwcjFKUSthcDROcwpZYms1ZUJLVjdyb3lFSVpRbEJTL1plWVgvaFFCOXpZMHIwZEVDQUJSL3FRZWZ6MjZUVjhCV2c3dnpDZDU0cXlLCktmZGRjZlRreFZGeVVDRGI3SHpYV28zOE1iMThWeVc5OXgyNjhWcUY3RmRiUkdlaUN1UmlLYmIwZUJTRVVBYkwKY3JKeDJPR3VXSU5FamlabTRsM1IxcFB1S1hHRGJyTU5oK0VkdHYrOFdVeFdFdFFzelMxT0lUUW4xUHo2UXYyZQpFckZEd2ViY1luTkFzaFlRaDE2eC9kSU54bFczQWdNQkFBR2pXVEJYTUE0R0ExVWREd0VCL3dRRUF3SUNwREFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUIwR0ExVWREZ1FXQkJRanYzRFI5TjY0Z2xPc1FQQXVjRVFkNW5vSHVqQVYKQmdOVkhSRUVEakFNZ2dwcmRXSmxjbTVsZEdWek1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQ1FhbVZCMmY1ZApuZ1FsdWY5a2JuNkZQYThjczQxUE5MVkxHT3VXMXMzbEEvUkdoZnp2RnI5MDFsTHVtYWFIR2s0TzFRNmkyNmtTCjRkd2g3cXVkMkFXMHVwWGthalVxTzhzK3FtUzlQeGxUdHIrbXJwZHg4WXNGMkdQbWlLMmxiTnNkeTVpVm50di8KQzFxMXNKV2dGbkVjOUh2QXQvQ3JSK3Ixcy9nMFc2Yk9aUVI2U3BISUd3YjE3azgvcTZUTVVaWWZlblp3bWU1KwpLODYxdW9lQXN5aWMwN2ptZUVZM0RaWUlXYWxCTWJySXd6MDUzTGJIbW1CTUpaT3IvL1ZiUzRYV0UvODRoa1F4CkdqczRrT3hudEpma0xUQlhoTTFxUlBNVjBHdkFsRXlDbFJTYnVybmI0clJLS1JTc0piTFBBbjZGVU5lcUdIcEMKSVgweGVPWXBudlRWCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + server: https://127.0.0.1:51697 + name: kind-dns-downstream +contexts: +- context: + cluster: kind-dns-downstream + user: kind-dns-downstream + name: kind-dns-downstream +current-context: kind-dns-downstream +kind: Config +users: +- name: kind-dns-downstream + user: + client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURLVENDQWhHZ0F3SUJBZ0lJRXFNTkJnTjFqVDB3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TmpBeE1USXlNRE0wTVRoYUZ3MHlOekF4TVRJeU1ETTVNVGhhTUR3eApIekFkQmdOVkJBb1RGbXQxWW1WaFpHMDZZMngxYzNSbGNpMWhaRzFwYm5NeEdUQVhCZ05WQkFNVEVHdDFZbVZ5CmJtVjBaWE10WVdSdGFXNHdnZ0VpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCRHdBd2dnRUtBb0lCQVFEVXI4NjEKWk5Rczl1MGpHSnBZYW9BeG1LTUVVdFIrZ2Z1aDFKSDBxK1Z2TzlpV2JSQ2ZCN2hnR1htUGVyQVFPM2tGekJleQoxTjIyWXhqQ0YwQUNVMUhNbVFpclh6OUlBMm44TCtoQW9QZlJqZ2JlZUxrNEJjL24wTU5aS3pMbStNTnY2bVFuCnlndC9ZYTFhdzN5d09rdjlPdVo0RjNiMzk2ZjBldjBwZlkvSnZKMWVvaWVCN09wUHJDaGpwcytBTzEwYUt1Ty8KemRLOHZTZE5ZN1J3TXlCY01wZ3JzQ3QyNUoyQm1kRFo0aldxUmpjL3QwVHJ2Ry9YSlZkb2NhYVJoSUtmVmg3WgpVWUVuRHZQd1BwWDJVK0JkYmVKMXRNNUpSRkJiVlpQdHNZUXgvODdHRW5QQmNpRC8xU0V4YmNMOFJtQlZHVlAyCkZaNmpkN2xhVVM1RGhuQ3JBZ01CQUFHalZqQlVNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUsKQmdnckJnRUZCUWNEQWpBTUJnTlZIUk1CQWY4RUFqQUFNQjhHQTFVZEl3UVlNQmFBRkNPL2NOSDAzcmlDVTZ4QQo4QzV3UkIzbWVnZTZNQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUIzU1lpZkZXSDZQMnQ3czJNTzFmWGNJdlJwCjVheXU3Nms2VHovYkU3ZlE5OGtJVERpVFh4NE1DVEpmVTExMEhBeFUxWXNXamczMUtPNERuNGZMZEhaL3BETHcKQWROVlFOa0s1UjlHOVl1bUZxTlh6WUw2YWh1OTVvUWNqdUNQQnRnMEdNOXNpeHZESWh2ejlaTkVNRldoSnJRRgpxNkR5M3o0THF2cjA2M1g1TzRPeHA0N0gwRm1vcTh5b2Y1aXE0d0RhTHNjOVhEbEYvdG9oSFhaTUxxbzJEN0tHCk50T2tTTnNDcy9SZXFONHBxcWdHN3FPWktoS0pDSkFiNmpiTG1tN3FSN0JZSHd4Umlqa25vZWc0NHJ6YmVITm0KMkhSZzJLZjlwSk5IV1lkSzhOV2YzUjdSN1R5QzJCb2daYUt3NWpwWXVTTXVWa2lseE1Kc1U3emQ2YmtYCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcFFJQkFBS0NBUUVBMUsvT3RXVFVMUGJ0SXhpYVdHcUFNWmlqQkZMVWZvSDdvZFNSOUt2bGJ6dllsbTBRCm53ZTRZQmw1ajNxd0VEdDVCY3dYc3RUZHRtTVl3aGRBQWxOUnpKa0lxMTgvU0FOcC9DL29RS0QzMFk0RzNuaTUKT0FYUDU5RERXU3N5NXZqRGIrcGtKOG9MZjJHdFdzTjhzRHBML1RybWVCZDI5L2VuOUhyOUtYMlB5YnlkWHFJbgpnZXpxVDZ3b1k2YlBnRHRkR2lyanY4M1N2TDBuVFdPMGNETWdYREtZSzdBcmR1U2RnWm5RMmVJMXFrWTNQN2RFCjY3eHYxeVZYYUhHbWtZU0NuMVllMlZHQkp3N3o4RDZWOWxQZ1hXM2lkYlRPU1VSUVcxV1Q3YkdFTWYvT3hoSnoKd1hJZy85VWhNVzNDL0VaZ1ZSbFQ5aFdlbzNlNVdsRXVRNFp3cXdJREFRQUJBb0lCQUF2OFNyWXp4elg4ckUrWgptdTkzemNXWFZOVTk4T2s0S1RBR3F3ZkZpYk0ybnZXWCtLTnFhNDk3b0FnVEZMQWI0M3B6d1lDSWEvNGdiUmttCktrVGRyZkxOUzFuR2p4MHNNT0UxdHJvWFBvWlRianVxZDQ4MFFYSG9Eam9tSzl5U3FxRFI3ZGI0THFUamJ3NnQKSmxSNW9Wa0lTckl2YWU0WUwrNXRGSzVncnd1dTBlRTNoeG9RYjNQMmF5TEI2aGd3RWRVS2hPcUtYSmQrM0JxbApSRHhoWDlMNGRxbU8venF1cE9TVk5iamtzTWV2ZGVKTDFycFZUU21KTWR3MEdsZjFXRjhMeFBqZVZ6R1ZxUjZyCjNYdkJkTmhLWlFoN1dZeXI0eDk5YVlCZnFpT0k5NUxDemFDdFBZQjBSejBiWWlZM3paTHJSRXlsd3FLK0Jxa04KZ0licjBhRUNnWUVBNmxtbWJrUlJMYVBwSkY2V3ZLVE1qVDNBS2tZVmIrRmdjdEt1S3JTY2RtOE5DVFJuY3d2SAphWWhvYjBHVldJR3I3cjkxNUxFTnZoa2hMWjBRcEJsVkdwMDkyazhXZ1BRTlllaHJ4cDhFeldUMTZpVjJYNVRpCmgxcjhTc04raU9VNThFa0JCQ1dOTElieGRtUWU2MTh6S1gyUlMzUUZqR3ZZS0pkUFAxeEdOa2tDZ1lFQTZGWFIKM0swRXNDTitpYmdodTE5Zlhla2xrOGJuNHZhOUZUSkdmTG5KSExEU1ErK250bkcvNEh5VkVLZGEzRnN2NWZNZQpTVUo0dDhHZnF4a0VIWCtxUzZ3VHVDUytRUnpMR0I4THFxRXB0bUdUMnJsTklCa2Z5RVNvWjRNN1VjNllkWS9sCmFDaW9ZenZMaWVDc2FtME9TeFozejljOEgzM0E2QlRSVFZhd0gxTUNnWUVBanYyc28xTmtCT2tpZEdLU3J3QVAKSDQ4eUZaazFzMUpkT3pKNXV1MEJHdktmamFKQURONS9DbEdGQjMySTFyd29ZRURLZW9QZDBzUWFqbTVyblBVbwpERmt0U0d0QlcrV04xTk93RHowdi9QTkJhV0Q2WFUvRytMZjNnTmJQK2srRGpxMjh4UDcwcU5xZHNwTmNtbGs0CktuVEhsclp3UEVJQlhxTVVZNkMxNXFFQ2dZRUFzNStGL01LWFdVWlgwa25WYW5PMTIza2hZRHJybElHR2RoakUKZmpGMDF3V3R5bkJDamI4cnhYY01HREFMQTBwTW9jOXdudHNSVWFBVXZjYzljMEQ4ZkR5eGtqQjJGd2tYeTdKVQo1cnBxOFdKSFdWYmgxZXNXczFMQmtDWFplc25xL1JrZkY0UTNpMkR6WDhtZ0F6Z0ZVUEF4K1RKQ2ZXWlAraDMrCkkzamQrWmtDZ1lFQWs5RW0zdzhOQ0tQbHkzWUlrTVN1YUVoSm82S0Zic09FN0FJZ1dHajBMS3hjUnNidVZ4VG0KSkU1ck9VVlFkcXZuR1NuSUo3TVo0TXNPcWptVklNV3NNUGNOQmNCTzAxbVZRUndxRTdVRXI4SWVKRUR4Z0laUwpOdzlTK0pOaTcyRnY2ZzkvWWtsUGZIbEhIWk5HRXdua2FnMytucDIwbTMvR2wzODNCckYzTGtzPQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo= + diff --git a/test/e2e/kubeconfig-upstream b/test/e2e/kubeconfig-upstream new file mode 100644 index 0000000..bd17f7a --- /dev/null +++ b/test/e2e/kubeconfig-upstream @@ -0,0 +1,19 @@ +apiVersion: v1 +clusters: +- cluster: + certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCVENDQWUyZ0F3SUJBZ0lJUlZobTdHdWtZN3d3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TmpBeE1USXlNRE0xTURGYUZ3MHpOakF4TVRBeU1EUXdNREZhTUJVeApFekFSQmdOVkJBTVRDbXQxWW1WeWJtVjBaWE13Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLCkFvSUJBUURIdjlFNVZKcFlqSDJHRUFQQkpRN3ZYMyt0SUNlVkI4Wmo4R25FRkJoMFd4VkN4TFF1cENWQ3kvU3IKcFBTblY4MHRYWUx1WHh0TDlkZkM3K0F2SEQySVJQbC9RVi80VVVQZkJqcHRuOEFDQm9XbEdsSlIwSkFrdnVSUQp0RnpxaFZwOWNnVHZFVm5aaEs4UjJpR3g5Z0FWcUJBRHFKS05oWmlBNm1lQXNDazQ2M3JzZ09oOCtsS0NGSlM1ClZCOEJ5MGl2UzVTSHo5eHJiNWJmUUxxcDVVNUZUQ1RMeUxiaHlTTDFBNjlQZGc3TzVuSHB4MEVBbXNtdFRPUlgKL3ZZNStZeFRQYlcrRkF3aTM2ZmlRZ0FmODNiQmRLdVdUOHpPeWwwYjJiczh2Um94dEI5LzdoeW9EYVhhRGRYcQp3V1kybGxCMW1YZmQ3eE1VMUxHVlVjZnpHcVgvQWdNQkFBR2pXVEJYTUE0R0ExVWREd0VCL3dRRUF3SUNwREFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUIwR0ExVWREZ1FXQkJSVFA4Vm83N0JvcjRnQ1g3SGZ2L0FvZUFyRWNEQVYKQmdOVkhSRUVEakFNZ2dwcmRXSmxjbTVsZEdWek1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQlFYYTg1L3UwQgpBUXJGbGVtcWpDaUZmTGJLRURpcTBSaWxNRmFWWUY0QnVrK0htcUpSbHA2ditRUytMR1U1NzljOXc1V3ExVlpUCnFuOE0zbkI1S1REeXRMVGFHaHhNSkpuODFSMjA3SWtudXZRdlZaWFZVOTZtd2FSQWdvQnlQNWpBVU9FZXJtclgKaGR2bWtDSzFHeTl1OHhpa0d3dmo2aURwUmxPSzlocUVkN0lvNFZrd0xtNFgreGtoR1UwK2RFaUluNkVjczRCKwpnR3BuZkEyb2tPUWh0bis2Tnhrd3JwekpPN0N2RVJpM3N3YmMvRkU3N3lsam9kUVNaZExVa1FrR1VuVDR6dVRmCmdYbEJ1ZE5Qc1doczEvR1hFTURpcUFDckZiaEF1WHpEU2RqTkpLeGRqbE04TmEvSjFBQUJ3TVJRSFFIL2I2dTcKRlZSV2RHbzJETzg0Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + server: https://127.0.0.1:51731 + name: kind-dns-upstream +contexts: +- context: + cluster: kind-dns-upstream + user: kind-dns-upstream + name: kind-dns-upstream +current-context: kind-dns-upstream +kind: Config +users: +- name: kind-dns-upstream + user: + client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURLVENDQWhHZ0F3SUJBZ0lJYUdHaGJrZTFUV0V3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TmpBeE1USXlNRE0xTURGYUZ3MHlOekF4TVRJeU1EUXdNREZhTUR3eApIekFkQmdOVkJBb1RGbXQxWW1WaFpHMDZZMngxYzNSbGNpMWhaRzFwYm5NeEdUQVhCZ05WQkFNVEVHdDFZbVZ5CmJtVjBaWE10WVdSdGFXNHdnZ0VpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCRHdBd2dnRUtBb0lCQVFERnpRVlMKZ2tmWENlTWtMekNCV0pOUXdCbnptQmdBVHFwYjZqaU5zT1RTL2NSMy93WksrU3RjRTljRmFmOUVMQWE1NVgvMQo4MExhdkhiVVVEc3pNM3lMRE5SbmZPYUFmWCtBOENRSzFYTWEyYnRXcGZOVDFWUzJTSGxaNlRNVTZyMDBIanVkCm83YTBCdmRxcDE3TUMrT2UzSUlXZnRmUVcwRUtLWHowWENlcjNibHlpRjBBa28ydmNUWFRtdmhpb25ERGtlSEkKVnVFVUN4YTQxb0FHcXBxWXo4VGltY2dYQ2QzQ3Vhdm9sUFhMdUpGN3NJNWRCNzNKNDlqN2Y5LzdiRWVqZGNadQpUc0hVT2NXOHdsOHJDNHN2NVZidWRoUk9vQ0VoblJ0U3gzWWcxdkN3cGw2QUFhS3FpNThwM1g2VklqKzZmMlZYCmtQUlZ4Ukp3aUl4ZUkyeTlBZ01CQUFHalZqQlVNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUsKQmdnckJnRUZCUWNEQWpBTUJnTlZIUk1CQWY4RUFqQUFNQjhHQTFVZEl3UVlNQmFBRkZNL3hXanZzR2l2aUFKZgpzZCsvOENoNENzUndNQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUJEL3owMWF2QkZFeERFWDduTHNBNGlRb3VhCm1kSmF2NWNsOTlJdTVZM2R6bXZ4anRJNk81VTlaNzdVeVFFK1VaTHl1KzZtKzVnbVZSQ1R3eVRNdWxxS1RaVWEKbDB1ZFFJTG9YbGhzR3NUdmJ1UmtnZTRTcnhVZWJZMFE3V3hsYzE1dTJmVThUQzZ0MWlrcW5rM09CMGZzM0puWQprSmdzQzNhNnZNaVppZjZYdTMvZHkrZmtYb2JMV3ZiaGxoYmZteXpTVDArRVoxa294M2FYTDMrcDF0TGZ3NzN3CmJoeWo5a1VYcEl1ZnJnNXN4L0VTdDRGMitSanVMbS9RN2FibkYzYkVXRFB4TURVRnV2WkJDQkI0YWQ0bW9KTmQKdU4yYnp3R3oybWxRbVFpcy9US1Nzd2REN2s0WVRER215eVRQYlh2b0l6RkdtTjNlcnZ1b0tWZFdtYTFtCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb2dJQkFBS0NBUUVBeGMwRlVvSkgxd25qSkM4d2dWaVRVTUFaODVnWUFFNnFXK280amJEazB2M0VkLzhHClN2a3JYQlBYQlduL1JDd0d1ZVYvOWZOQzJyeDIxRkE3TXpOOGl3elVaM3ptZ0gxL2dQQWtDdFZ6R3RtN1ZxWHoKVTlWVXRraDVXZWt6Rk9xOU5CNDduYU8ydEFiM2FxZGV6QXZqbnR5Q0ZuN1gwRnRCQ2lsODlGd25xOTI1Y29oZApBSktOcjNFMTA1cjRZcUp3dzVIaHlGYmhGQXNXdU5hQUJxcWFtTS9FNHBuSUZ3bmR3cm1yNkpUMXk3aVJlN0NPClhRZTl5ZVBZKzMvZisyeEhvM1hHYms3QjFEbkZ2TUpmS3d1TEwrVlc3bllVVHFBaElaMGJVc2QySU5id3NLWmUKZ0FHaXFvdWZLZDErbFNJL3VuOWxWNUQwVmNVU2NJaU1YaU5zdlFJREFRQUJBb0lCQUJUM3h4TG9SeHZMUzBZTQpiNUNpbGxrMngvbDcyNzE2bVZJTmllbXhRUXlCeEtnd3cxYkN3NThxNWo0SGJyMG9DcDE5cjlzZmFxeWIwbC91CjBsdTYzejZ4UVRub01sb1lFNkpVTW9ub2R4OTNTY1hsYVI0dnJOOTIzdEJTYVcwVDlqTVdhbHpyWENTSTRZVHYKa1p2QlBlT2EvZnBLLzI4eG9Uc2x5ejV2SDNCM0xIU0JVZWZlV25oYlY5V0pESXRRVHgvZ1pSUXpIUmNJbG5NdgpDeWhZUmpqVksrOXVGZDNRRWZWcDk3RjNHNXUwbzdqQXJRQWcyWThrUjZ0dDVaZkR4clhkdCs0Z2RhVTBHZFE4Cm8rUTNlR1VUVEhUQm9nSHJwdDRwcHZXV0pITU11YW95VTZiK2p3OXN2RUdqVWphODRZYjNZUllGUmJYeUJZc0oKeGszSU4vRUNnWUVBek9ETGhWYVdIWjVpNUtTblhucy9pNDFmNjVOL2Rja1V0UlhQM0RURm1iRTdKWldkWFZNWApSYm4vRU1mcDV5MzFSK0pWYVpQVlpnR2x1TTllL21JSWY4ejZkVTk3ZmJNU05NTUZYZDdLQkNOZnVhVllxdUdrCk1SZkhBQ2V4M0Z0N29obnROUjhQbnh4VVljMEpXUkd6L2VJeU11b2lGRXRNMGFoQlkzYVIrZTBDZ1lFQTl5Z2wKUnE3MTIxOXB5YnNFOUVXcmtHZmxDblh5WDI1aTVMZXdkd1AzUGNzNFNYVDhKZTllRUVPZDFRTEZ3R2JVZ2djNQp0bFFtcFVhMG52WmtDYlp6T3dPQk5nWUtBVEpBcjk3TEpDVGNNcFJ4Sk1BSWJQMG8zVkVTL0U2OXliUmMzOWQxClVac293RkRTejFxd1I0WGdycTgxZ3FJZE45emNReENWS0NEdXBCRUNnWUJ2VWFFanBPVlIySkpSTzJtNU8yeE8KamhWVk1jSnFwRVE5RkVucGt6N2VnRjdyei93K0RmeXlKUnFDNnF5YnNPdjZEKzlxdXltVEVFZ1VQNUNVMVgxYQp1MnhHdTFZVStXeG1BS1QwMlMyWXpBT2lJa1lvS3d3RXBLKzYxTmFlTFpMaWhBWFAvRDJIcldQbjgva2xUU29vClEzUVZHQVJHVkplN3Z4a3dTdWVNRFFLQmdFM05EMTdldUhuajRSTWxrZnVxNnNTOFQ3Y3BSYkNRdVFTeVpoUXcKNVdWSVVXR2VON2xoVGtUa1pBeW5vTVJlR2tzTUp6aWo2TDVpTVgxUXBsRUFZK21Sd3R6VXJkV09raHBLa2J2QQo5cWZkWG5ocEVyM3NPeTdmMUpBajRVNWJQbGtnSThnYWhZdDBaY2ZzRGsyVmNSTE1DSllrbmZuMXhrZytNaFc5CnVDRmhBb0dBSEw3SGVZZTlxQzgvSDh4a0l2MnYrRE1WT1hSWVdwMStLZEhMbkUwWkZIdDhIdDlNWmZiRkZWM0wKMk1taDVWMUNLUS9CN3VNOWVhUW1SZVJDWEExcUZTQlhWaG8rd1daSWh0WnhmVFZVQWVLWkR1U0VjanM4MGlRVApMbmlMbWtrSFFhd01GVnJhSWFKczYvUEc0QjZheDFGRTZDSS85T2lDaXZzMXdwMlFKWjQ9Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg== + diff --git a/test/e2e/tsig/chainsaw-test.yaml b/test/e2e/tsig/chainsaw-test.yaml new file mode 100644 index 0000000..025c44a --- /dev/null +++ b/test/e2e/tsig/chainsaw-test.yaml @@ -0,0 +1,491 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/chainsaw/main/.schemas/json/test-chainsaw-v1alpha1.json +apiVersion: chainsaw.kyverno.io/v1alpha1 +kind: Test +metadata: + name: dns-operator-tsig +spec: + clusters: + upstream: + kubeconfig: ../kubeconfig-upstream + downstream: + kubeconfig: ../kubeconfig-downstream + cluster: upstream + steps: + - name: Prereq - create DNSZoneClass in upstream + try: + - create: + cluster: upstream + resource: + apiVersion: dns.networking.miloapis.com/v1alpha1 + kind: DNSZoneClass + metadata: + name: powerdns-static-tsig + spec: + controllerName: powerdns + nameServerPolicy: + mode: Static + static: + servers: + - ns1.example.com + - ns2.example.com + - assert: + cluster: upstream + resource: + apiVersion: dns.networking.miloapis.com/v1alpha1 + kind: DNSZoneClass + metadata: + name: powerdns-static-tsig + + - name: Prereq - create DNSZoneClass in downstream + try: + - create: + cluster: downstream + resource: + apiVersion: dns.networking.miloapis.com/v1alpha1 + kind: DNSZoneClass + metadata: + name: powerdns-static-tsig + spec: + controllerName: powerdns + nameServerPolicy: + mode: Static + static: + servers: + - ns1.example.com + - ns2.example.com + - assert: + cluster: downstream + resource: + apiVersion: dns.networking.miloapis.com/v1alpha1 + kind: DNSZoneClass + metadata: + name: powerdns-static-tsig + + - name: Create DNSZone upstream + try: + - create: + cluster: upstream + resource: + apiVersion: dns.networking.miloapis.com/v1alpha1 + kind: DNSZone + metadata: + name: example-com-tsig + spec: + domainName: example-tsig.com + dnsZoneClassName: powerdns-static-tsig + - assert: + cluster: upstream + resource: + apiVersion: dns.networking.miloapis.com/v1alpha1 + kind: DNSZone + metadata: + name: example-com-tsig + + - name: Wait for DNSZone Accepted/Programmed upstream + try: + - sleep: + duration: 5s + - assert: + cluster: upstream + resource: + apiVersion: dns.networking.miloapis.com/v1alpha1 + kind: DNSZone + metadata: + name: example-com-tsig + status: + conditions: + - type: Accepted + status: "True" + - type: Programmed + status: "True" + + - name: Confirm DNSZone exists downstream in mapped namespace + try: + - script: + cluster: upstream + skipCommandOutput: true + skipLogOutput: true + content: | + kubectl get ns $NAMESPACE -o json + outputs: + - name: downstreamNamespaceName + value: (join('-', ['ns', json_parse($stdout).metadata.uid])) + - assert: + cluster: downstream + resource: + apiVersion: dns.networking.miloapis.com/v1alpha1 + kind: DNSZone + metadata: + name: example-com-tsig + namespace: ($downstreamNamespaceName) + + - name: Create generated TSIGKey upstream (no secretRef) + try: + - create: + cluster: upstream + resource: + apiVersion: dns.networking.miloapis.com/v1alpha1 + kind: TSIGKey + metadata: + name: example-com-tsig-gen + spec: + dnsZoneRef: + name: example-com-tsig + keyName: datum-example-com-tsig-gen + algorithm: hmac-sha256 + - assert: + cluster: upstream + resource: + apiVersion: dns.networking.miloapis.com/v1alpha1 + kind: TSIGKey + metadata: + name: example-com-tsig-gen + ownerReferences: + - apiVersion: dns.networking.miloapis.com/v1alpha1 + kind: DNSZone + name: example-com-tsig + + - name: Wait for generated TSIGKey Accepted/Programmed upstream + try: + - sleep: + duration: 5s + - assert: + cluster: upstream + resource: + apiVersion: dns.networking.miloapis.com/v1alpha1 + kind: TSIGKey + metadata: + name: example-com-tsig-gen + status: + conditions: + - type: Accepted + status: "True" + - type: Programmed + status: "True" + + - name: Assert generated Secret exists upstream + try: + - assert: + cluster: upstream + resource: + apiVersion: v1 + kind: Secret + metadata: + name: example-com-tsig-gen + + - name: Confirm generated TSIGKey + Secret exist downstream + try: + - script: + cluster: upstream + skipCommandOutput: true + skipLogOutput: true + content: | + kubectl get ns $NAMESPACE -o json + outputs: + - name: downstreamNamespaceName + value: (join('-', ['ns', json_parse($stdout).metadata.uid])) + - assert: + cluster: downstream + resource: + apiVersion: dns.networking.miloapis.com/v1alpha1 + kind: TSIGKey + metadata: + name: example-com-tsig-gen + namespace: ($downstreamNamespaceName) + - assert: + cluster: downstream + resource: + apiVersion: v1 + kind: Secret + metadata: + name: example-com-tsig-gen + namespace: ($downstreamNamespaceName) + + - name: Delete downstream generated Secret and ensure it is recreated + try: + - script: + cluster: upstream + skipCommandOutput: true + skipLogOutput: true + content: | + kubectl get ns $NAMESPACE -o json + outputs: + - name: downstreamNamespaceName + value: (join('-', ['ns', json_parse($stdout).metadata.uid])) + - delete: + cluster: downstream + ref: + apiVersion: v1 + kind: Secret + namespace: ($downstreamNamespaceName) + name: example-com-tsig-gen + - sleep: + duration: 5s + - assert: + cluster: downstream + resource: + apiVersion: v1 + kind: Secret + metadata: + name: example-com-tsig-gen + namespace: ($downstreamNamespaceName) + + - name: Delete downstream generated TSIGKey and ensure it + Secret are recreated + try: + - script: + cluster: upstream + skipCommandOutput: true + skipLogOutput: true + content: | + kubectl get ns $NAMESPACE -o json + outputs: + - name: downstreamNamespaceName + value: (join('-', ['ns', json_parse($stdout).metadata.uid])) + - script: + cluster: downstream + content: | + set -euo pipefail + # Find the mapped downstream namespace by label selector, then delete. + ns=$(kubectl get tsigkey -A \ + -l meta.datumapis.com/upstream-namespace="$NAMESPACE",meta.datumapis.com/upstream-name=example-com-tsig-gen \ + -o jsonpath='{.items[0].metadata.namespace}') + kubectl -n "$ns" delete tsigkey example-com-tsig-gen --wait=false --ignore-not-found=true + - sleep: + duration: 8s + - assert: + cluster: downstream + resource: + apiVersion: dns.networking.miloapis.com/v1alpha1 + kind: TSIGKey + metadata: + name: example-com-tsig-gen + namespace: ($downstreamNamespaceName) + - assert: + cluster: downstream + resource: + apiVersion: v1 + kind: Secret + metadata: + name: example-com-tsig-gen + namespace: ($downstreamNamespaceName) + + - name: Delete upstream generated TSIGKey and ensure downstream TSIGKey + Secret are GC'd + try: + - script: + cluster: upstream + skipCommandOutput: true + skipLogOutput: true + content: | + kubectl get ns $NAMESPACE -o json + outputs: + - name: downstreamNamespaceName + value: (join('-', ['ns', json_parse($stdout).metadata.uid])) + - delete: + cluster: upstream + ref: + apiVersion: dns.networking.miloapis.com/v1alpha1 + kind: TSIGKey + name: example-com-tsig-gen + - sleep: + duration: 10s + - error: + cluster: downstream + resource: + apiVersion: dns.networking.miloapis.com/v1alpha1 + kind: TSIGKey + metadata: + name: example-com-tsig-gen + namespace: ($downstreamNamespaceName) + - error: + cluster: downstream + resource: + apiVersion: v1 + kind: Secret + metadata: + name: example-com-tsig-gen + namespace: ($downstreamNamespaceName) + + - name: Create BYO Secret + TSIGKey upstream + try: + - create: + cluster: upstream + resource: + apiVersion: v1 + kind: Secret + metadata: + name: byo-tsig + type: Opaque + stringData: + secret: supersecret + - create: + cluster: upstream + resource: + apiVersion: dns.networking.miloapis.com/v1alpha1 + kind: TSIGKey + metadata: + name: example-com-tsig-byo + spec: + dnsZoneRef: + name: example-com-tsig + keyName: datum-example-com-tsig-byo + algorithm: hmac-sha256 + secretRef: + name: byo-tsig + + - name: Wait for BYO TSIGKey Accepted/Programmed upstream + try: + - sleep: + duration: 5s + - assert: + cluster: upstream + resource: + apiVersion: dns.networking.miloapis.com/v1alpha1 + kind: TSIGKey + metadata: + name: example-com-tsig-byo + status: + conditions: + - type: Accepted + status: "True" + - type: Programmed + status: "True" + + - name: Assert BYO Secret exists upstream + try: + - assert: + cluster: upstream + resource: + apiVersion: v1 + kind: Secret + metadata: + name: byo-tsig + + - name: Confirm BYO Secret exists downstream and recreates when deleted + try: + - script: + cluster: upstream + skipCommandOutput: true + skipLogOutput: true + content: | + kubectl get ns $NAMESPACE -o json + outputs: + - name: downstreamNamespaceName + value: (join('-', ['ns', json_parse($stdout).metadata.uid])) + - assert: + cluster: downstream + resource: + apiVersion: v1 + kind: Secret + metadata: + name: byo-tsig + namespace: ($downstreamNamespaceName) + - delete: + cluster: downstream + ref: + apiVersion: v1 + kind: Secret + namespace: ($downstreamNamespaceName) + name: byo-tsig + - sleep: + duration: 5s + - assert: + cluster: downstream + resource: + apiVersion: v1 + kind: Secret + metadata: + name: byo-tsig + namespace: ($downstreamNamespaceName) + + - name: Edge case - bad secret surfaced via Accepted=False (missing secret key) + try: + - create: + cluster: upstream + resource: + apiVersion: v1 + kind: Secret + metadata: + name: bad-tsig + type: Opaque + stringData: + notsecret: foo + - create: + cluster: upstream + resource: + apiVersion: dns.networking.miloapis.com/v1alpha1 + kind: TSIGKey + metadata: + name: example-com-tsig-bad + spec: + dnsZoneRef: + name: example-com-tsig + keyName: datum-example-com-tsig-bad + algorithm: hmac-sha256 + secretRef: + name: bad-tsig + - sleep: + duration: 5s + - assert: + cluster: upstream + resource: + apiVersion: dns.networking.miloapis.com/v1alpha1 + kind: TSIGKey + metadata: + name: example-com-tsig-bad + status: + conditions: + - type: Accepted + status: "False" + reason: InvalidSecret + + - name: Teardown — delete keys, zone, classes + try: + - delete: + cluster: upstream + ref: + apiVersion: dns.networking.miloapis.com/v1alpha1 + kind: TSIGKey + name: example-com-tsig-gen + - delete: + cluster: upstream + ref: + apiVersion: dns.networking.miloapis.com/v1alpha1 + kind: TSIGKey + name: example-com-tsig-byo + - delete: + cluster: upstream + ref: + apiVersion: dns.networking.miloapis.com/v1alpha1 + kind: TSIGKey + name: example-com-tsig-bad + - delete: + cluster: upstream + ref: + apiVersion: v1 + kind: Secret + name: byo-tsig + - delete: + cluster: upstream + ref: + apiVersion: v1 + kind: Secret + name: bad-tsig + - delete: + cluster: upstream + ref: + apiVersion: dns.networking.miloapis.com/v1alpha1 + kind: DNSZone + name: example-com-tsig + - delete: + cluster: downstream + ref: + apiVersion: dns.networking.miloapis.com/v1alpha1 + kind: DNSZoneClass + name: powerdns-static-tsig + - delete: + cluster: upstream + ref: + apiVersion: dns.networking.miloapis.com/v1alpha1 + kind: DNSZoneClass + name: powerdns-static-tsig + diff --git a/test/e2e/chainsaw-test.yaml b/test/e2e/zones-and-records/chainsaw-test.yaml similarity index 98% rename from test/e2e/chainsaw-test.yaml rename to test/e2e/zones-and-records/chainsaw-test.yaml index 7d98d25..c816986 100644 --- a/test/e2e/chainsaw-test.yaml +++ b/test/e2e/zones-and-records/chainsaw-test.yaml @@ -2,13 +2,13 @@ apiVersion: chainsaw.kyverno.io/v1alpha1 kind: Test metadata: - name: dns-operator-end-to-end + name: dns-operator-zones-and-records spec: clusters: upstream: - kubeconfig: kubeconfig-upstream + kubeconfig: ../kubeconfig-upstream downstream: - kubeconfig: kubeconfig-downstream + kubeconfig: ../kubeconfig-downstream cluster: upstream steps: - name: Prereq - create DNSZoneClass in upstream @@ -404,3 +404,4 @@ spec: kind: DNSZoneClass check: ($error == null): true + From 98a2802f06526138b2a8744dee88a04686f2e15b Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Tue, 13 Jan 2026 11:57:53 -0800 Subject: [PATCH 2/4] feat: tsigkey implementation --- ...igkey_types.go => dnszonetsigkey_types.go} | 0 cmd/main.go | 8 +- config/crd/kustomization.yaml | 3 +- config/rbac/role.yaml | 6 +- ... => dnszonetsigkey_powerdns_controller.go} | 140 ++++++++++------- ...nszonetsigkey_powerdns_controller_test.go} | 83 ++++++----- ...> dnszonetsigkey_replicator_controller.go} | 141 +++++++++++------- ...szonetsigkey_replicator_controller_test.go | 34 +++++ test/e2e/kubeconfig-downstream | 8 +- test/e2e/kubeconfig-upstream | 8 +- test/e2e/tsig/chainsaw-test.yaml | 48 +++--- 11 files changed, 292 insertions(+), 187 deletions(-) rename api/v1alpha1/{tsigkey_types.go => dnszonetsigkey_types.go} (100%) rename internal/controller/{tsigkey_powerdns_controller.go => dnszonetsigkey_powerdns_controller.go} (70%) rename internal/controller/{tsigkey_powerdns_controller_test.go => dnszonetsigkey_powerdns_controller_test.go} (69%) rename internal/controller/{tsigkey_replicator_controller.go => dnszonetsigkey_replicator_controller.go} (67%) create mode 100644 internal/controller/dnszonetsigkey_replicator_controller_test.go diff --git a/api/v1alpha1/tsigkey_types.go b/api/v1alpha1/dnszonetsigkey_types.go similarity index 100% rename from api/v1alpha1/tsigkey_types.go rename to api/v1alpha1/dnszonetsigkey_types.go diff --git a/cmd/main.go b/cmd/main.go index bdbd1ec..d4acc3f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -231,11 +231,11 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "DNSRecordSetPowerDNS") os.Exit(1) } - if err := (&controller.TSIGKeyPowerDNSReconciler{ + if err := (&controller.DNSZoneTSIGKeyPowerDNSReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "TSIGKeyPowerDNS") + setupLog.Error(err, "unable to create controller", "controller", "DNSZoneTSIGKeyPowerDNS") os.Exit(1) } @@ -313,10 +313,10 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "DNSZoneDiscoveryReplicator") os.Exit(1) } - if err := (&controller.TSIGKeyReplicator{ + if err := (&controller.DNSZoneTSIGKeyReplicator{ DownstreamClient: downstreamCluster.GetClient(), }).SetupWithManager(mcmgr, downstreamCluster); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "TSIGKeyReplicator") + setupLog.Error(err, "unable to create controller", "controller", "DNSZoneTSIGKeyReplicator") os.Exit(1) } diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 66d60bd..3032606 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -9,7 +9,8 @@ resources: - bases/dns.networking.miloapis.com_dnszonetsigkeys.yaml # +kubebuilder:scaffold:crdkustomizeresource -patches: +# kustomize expects this to be an array; keep empty by default. +patches: [] # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. # patches here are for enabling the conversion webhook for each CRD # +kubebuilder:scaffold:crdkustomizewebhookpatch diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 7cdd26a..b9e95d8 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -45,7 +45,7 @@ rules: - dns.networking.miloapis.com resources: - dnsrecordsets/finalizers - - tsigkeys/finalizers + - dnszonetsigkeys/finalizers verbs: - update - apiGroups: @@ -54,7 +54,7 @@ rules: - dnsrecordsets/status - dnszonediscoveries/status - dnszones/status - - tsigkeys/status + - dnszonetsigkeys/status verbs: - get - patch @@ -71,7 +71,7 @@ rules: - dns.networking.miloapis.com resources: - dnszonediscoveries - - tsigkeys + - dnszonetsigkeys verbs: - get - list diff --git a/internal/controller/tsigkey_powerdns_controller.go b/internal/controller/dnszonetsigkey_powerdns_controller.go similarity index 70% rename from internal/controller/tsigkey_powerdns_controller.go rename to internal/controller/dnszonetsigkey_powerdns_controller.go index 582c9b8..bbed702 100644 --- a/internal/controller/tsigkey_powerdns_controller.go +++ b/internal/controller/dnszonetsigkey_powerdns_controller.go @@ -6,6 +6,7 @@ import ( "context" "encoding/base64" "fmt" + "strings" "time" dnsv1alpha1 "go.miloapis.com/dns-operator/api/v1alpha1" @@ -22,61 +23,94 @@ import ( logf "sigs.k8s.io/controller-runtime/pkg/log" ) -const tsigKeyPowerDNSFinalizer = "dns.networking.miloapis.com/finalize-tsigkey-powerdns" +const dnsZoneTSIGKeyPowerDNSFinalizer = "dns.networking.miloapis.com/finalize-dnszonetsigkey-powerdns" -// TSIGKeyPDNS is the subset of the PowerDNS client used by the TSIGKey controller. -type TSIGKeyPDNS interface { +// DNSZoneTSIGKeyPDNS is the subset of the PowerDNS client used by the DNSZoneTSIGKey controller. +type DNSZoneTSIGKeyPDNS interface { EnsureTSIGKey(ctx context.Context, name, algorithm, keyMaterial string) (pdnsclient.TSIGKey, error) DeleteTSIGKey(ctx context.Context, id string) error - DeleteTSIGKeyByName(ctx context.Context, name string) error } -// TSIGKeyPowerDNSReconciler programs TSIG keys into PowerDNS. -type TSIGKeyPowerDNSReconciler struct { +// DNSZoneTSIGKeyPowerDNSReconciler programs TSIG keys into PowerDNS. +type DNSZoneTSIGKeyPowerDNSReconciler struct { client.Client Scheme *runtime.Scheme // PDNS is optional; when nil, SetupWithManager constructs one from env via pdnsclient.NewFromEnv(). - PDNS TSIGKeyPDNS + PDNS DNSZoneTSIGKeyPDNS } -// +kubebuilder:rbac:groups=dns.networking.miloapis.com,resources=tsigkeys,verbs=get;list;watch;update;patch -// +kubebuilder:rbac:groups=dns.networking.miloapis.com,resources=tsigkeys/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=dns.networking.miloapis.com,resources=tsigkeys/finalizers,verbs=update +func qualifyTSIGKeyName(keyName, zoneDomain string) string { + // PowerDNS TSIG keys are stored by (DNS) name. Ensure we always send an FQDN + // for the key name by appending the zone domain when keyName is not already + // zone-qualified. + // + // Trailing dots are optional; preserve whether the user supplied one. + keyName = strings.TrimSpace(keyName) + zoneDomain = strings.TrimSpace(zoneDomain) + if keyName == "" || zoneDomain == "" { + return keyName + } + + hasTrailingDot := strings.HasSuffix(keyName, ".") + keyNoDot := strings.TrimSuffix(keyName, ".") + zoneNoDot := strings.TrimSuffix(zoneDomain, ".") + + lKey := strings.ToLower(keyNoDot) + lZone := strings.ToLower(zoneNoDot) + + // Already qualified for this zone (or equals zone itself). + if lKey == lZone || strings.HasSuffix(lKey, "."+lZone) { + if hasTrailingDot { + return keyNoDot + "." + } + return keyNoDot + } + + qualified := keyNoDot + "." + zoneNoDot + if hasTrailingDot { + return qualified + "." + } + return qualified +} + +// +kubebuilder:rbac:groups=dns.networking.miloapis.com,resources=dnszonetsigkeys,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=dns.networking.miloapis.com,resources=dnszonetsigkeys/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=dns.networking.miloapis.com,resources=dnszonetsigkeys/finalizers,verbs=update // +kubebuilder:rbac:groups=dns.networking.miloapis.com,resources=dnszones,verbs=get;list;watch // +kubebuilder:rbac:groups=dns.networking.miloapis.com,resources=dnszoneclasses,verbs=get;list;watch // +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch;create;update;patch;delete -func (r *TSIGKeyPowerDNSReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { +func (r *DNSZoneTSIGKeyPowerDNSReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { logger := logf.FromContext(ctx).WithValues("namespace", req.Namespace, "name", req.Name) - logger.Info("tsigkey powerdns reconcile start") + logger.Info("dnszonetsigkey powerdns reconcile start") - var tk dnsv1alpha1.TSIGKey + var tk dnsv1alpha1.DNSZoneTSIGKey if err := r.Get(ctx, req.NamespacedName, &tk); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } // Deletion path: delete from PDNS, then drop finalizer. if !tk.DeletionTimestamp.IsZero() { - if controllerutil.ContainsFinalizer(&tk, tsigKeyPowerDNSFinalizer) { + if controllerutil.ContainsFinalizer(&tk, dnsZoneTSIGKeyPowerDNSFinalizer) { pdnsCli := r.PDNS if pdnsCli == nil { return ctrl.Result{}, fmt.Errorf("pdns client is nil (SetupWithManager not called?)") } // Best-effort cleanup by ID. - if tk.Status.TSIGKeyID != "" { - if err := pdnsCli.DeleteTSIGKey(ctx, tk.Status.TSIGKeyID); err != nil { - logger.Error(err, "failed to delete PDNS TSIG key by id; will retry", "id", tk.Status.TSIGKeyID) + if tk.Status.TSIGKeyName != "" { + if err := pdnsCli.DeleteTSIGKey(ctx, tk.Status.TSIGKeyName); err != nil { + logger.Error(err, "failed to delete PDNS TSIG key by id; will retry", "id", tk.Status.TSIGKeyName) return ctrl.Result{}, err } } else { // If we don't have a provider ID, we can't safely delete an external key. // This commonly means the key was never successfully programmed. - logger.Info("skipping PDNS TSIG key delete (missing status.tsigKeyID)") + logger.Info("skipping PDNS TSIG key delete (missing status.tsigKeyName)") } base := tk.DeepCopy() - controllerutil.RemoveFinalizer(&tk, tsigKeyPowerDNSFinalizer) + controllerutil.RemoveFinalizer(&tk, dnsZoneTSIGKeyPowerDNSFinalizer) if err := r.Patch(ctx, &tk, client.MergeFrom(base)); err != nil { return ctrl.Result{}, err } @@ -85,16 +119,16 @@ func (r *TSIGKeyPowerDNSReconciler) Reconcile(ctx context.Context, req ctrl.Requ } // Ensure finalizer while active. - if !controllerutil.ContainsFinalizer(&tk, tsigKeyPowerDNSFinalizer) { + if !controllerutil.ContainsFinalizer(&tk, dnsZoneTSIGKeyPowerDNSFinalizer) { base := tk.DeepCopy() - controllerutil.AddFinalizer(&tk, tsigKeyPowerDNSFinalizer) + controllerutil.AddFinalizer(&tk, dnsZoneTSIGKeyPowerDNSFinalizer) if err := r.Patch(ctx, &tk, client.MergeFrom(base)); err != nil { return ctrl.Result{}, err } return ctrl.Result{}, nil } - // Resolve zone + class and ensure this TSIGKey is for a PowerDNS zone. + // Resolve zone + class and ensure this DNSZoneTSIGKey is for a PowerDNS zone. zone, ok, err := r.resolveZone(ctx, &tk) if err != nil { return ctrl.Result{}, err @@ -112,7 +146,7 @@ func (r *TSIGKeyPowerDNSReconciler) Reconcile(ctx context.Context, req ctrl.Requ return ctrl.Result{}, nil } - // Ensure the DNSZone is an owner of this TSIGKey so GC cascades on zone deletion. + // Ensure the DNSZone is an owner of this DNSZoneTSIGKey so GC cascades on zone deletion. if !metav1.IsControlledBy(&tk, zone) { base := tk.DeepCopy() if err := controllerutil.SetControllerReference(zone, &tk, r.Scheme); err != nil { @@ -135,12 +169,9 @@ func (r *TSIGKeyPowerDNSReconciler) Reconcile(ctx context.Context, req ctrl.Requ if err != nil { return ctrl.Result{}, err } - if !ok { - // invalid secret schema etc; Accepted updated - return ctrl.Result{}, nil - } - // Update status.secretName if needed. + // Update status.secretName if needed (even when the Secret is not present yet). + // This ensures Secret watch events can enqueue the owning DNSZoneTSIGKey. if secretName != "" && tk.Status.SecretName != secretName { base := tk.DeepCopy() tk.Status.SecretName = secretName @@ -149,21 +180,28 @@ func (r *TSIGKeyPowerDNSReconciler) Reconcile(ctx context.Context, req ctrl.Requ } // re-fetch not required; continue } + if !ok { + // invalid secret schema etc; Accepted updated + return ctrl.Result{}, nil + } if err := r.setAcceptedCondition(ctx, &tk, metav1.ConditionTrue, ReasonAccepted, "Accepted for zone"); err != nil { return ctrl.Result{}, err } - created, pdnsErr := r.PDNS.EnsureTSIGKey(ctx, tk.Spec.KeyName, string(alg), keyMaterial) + ensureName := qualifyTSIGKeyName(tk.Spec.KeyName, zone.Spec.DomainName) + created, pdnsErr := r.PDNS.EnsureTSIGKey(ctx, ensureName, string(alg), keyMaterial) if pdnsErr != nil { _ = r.setProgrammedCondition(ctx, &tk, metav1.ConditionFalse, ReasonPDNSError, pdnsErr.Error()) return ctrl.Result{}, pdnsErr } - // Persist provider ID. - if created.ID != "" && tk.Status.TSIGKeyID != created.ID { + // Persist provider identifier (PowerDNS ID). This is 1:1 with the TSIG key name in PDNS, + // but we expose it under status.tsigKeyName to align with the upstream "wire name". + id := created.ID + if id != "" && tk.Status.TSIGKeyName != id { base := tk.DeepCopy() - tk.Status.TSIGKeyID = created.ID + tk.Status.TSIGKeyName = id if err := r.Status().Patch(ctx, &tk, client.MergeFrom(base)); err != nil { return ctrl.Result{}, err } @@ -173,11 +211,11 @@ func (r *TSIGKeyPowerDNSReconciler) Reconcile(ctx context.Context, req ctrl.Requ return ctrl.Result{}, err } - logger.Info("tsigkey powerdns reconcile complete") + logger.Info("dnszonetsigkey powerdns reconcile complete") return ctrl.Result{}, nil } -func (r *TSIGKeyPowerDNSReconciler) resolveZone(ctx context.Context, tk *dnsv1alpha1.TSIGKey) (*dnsv1alpha1.DNSZone, bool, error) { +func (r *DNSZoneTSIGKeyPowerDNSReconciler) resolveZone(ctx context.Context, tk *dnsv1alpha1.DNSZoneTSIGKey) (*dnsv1alpha1.DNSZone, bool, error) { // Zone lookup var zone dnsv1alpha1.DNSZone if err := r.Get(ctx, client.ObjectKey{Namespace: tk.Namespace, Name: tk.Spec.DNSZoneRef.Name}, &zone); err != nil { @@ -200,7 +238,7 @@ func (r *TSIGKeyPowerDNSReconciler) resolveZone(ctx context.Context, tk *dnsv1al return &zone, true, nil } -func (r *TSIGKeyPowerDNSReconciler) resolveZoneClass(ctx context.Context, tk *dnsv1alpha1.TSIGKey, zone *dnsv1alpha1.DNSZone) (*dnsv1alpha1.DNSZoneClass, bool, error) { +func (r *DNSZoneTSIGKeyPowerDNSReconciler) resolveZoneClass(ctx context.Context, tk *dnsv1alpha1.DNSZoneTSIGKey, zone *dnsv1alpha1.DNSZone) (*dnsv1alpha1.DNSZoneClass, bool, error) { if zone.Spec.DNSZoneClassName == "" { if err := r.setAcceptedCondition(ctx, tk, metav1.ConditionFalse, ReasonPending, fmt.Sprintf("DNSZone %q has no class yet", zone.Name)); err != nil { @@ -231,7 +269,7 @@ func (r *TSIGKeyPowerDNSReconciler) resolveZoneClass(ctx context.Context, tk *dn return &zc, true, nil } -func (r *TSIGKeyPowerDNSReconciler) resolveKeyMaterial(ctx context.Context, tk *dnsv1alpha1.TSIGKey, algorithm string) (secretName string, keyMaterial string, ok bool, err error) { +func (r *DNSZoneTSIGKeyPowerDNSReconciler) resolveKeyMaterial(ctx context.Context, tk *dnsv1alpha1.DNSZoneTSIGKey, algorithm string) (secretName string, keyMaterial string, ok bool, err error) { // BYO secret: validate schema and do not mutate. if tk.Spec.SecretRef != nil && tk.Spec.SecretRef.Name != "" { var s corev1.Secret @@ -271,7 +309,7 @@ func (r *TSIGKeyPowerDNSReconciler) resolveKeyMaterial(ctx context.Context, tk * return secretName, base64.StdEncoding.EncodeToString(secB), true, nil } -func (r *TSIGKeyPowerDNSReconciler) setAcceptedCondition(ctx context.Context, tk *dnsv1alpha1.TSIGKey, status metav1.ConditionStatus, reason, message string) error { +func (r *DNSZoneTSIGKeyPowerDNSReconciler) setAcceptedCondition(ctx context.Context, tk *dnsv1alpha1.DNSZoneTSIGKey, status metav1.ConditionStatus, reason, message string) error { base := tk.DeepCopy() cond := metav1.Condition{ Type: CondAccepted, @@ -287,7 +325,7 @@ func (r *TSIGKeyPowerDNSReconciler) setAcceptedCondition(ctx context.Context, tk return r.Status().Patch(ctx, tk, client.MergeFrom(base)) } -func (r *TSIGKeyPowerDNSReconciler) setProgrammedCondition(ctx context.Context, tk *dnsv1alpha1.TSIGKey, status metav1.ConditionStatus, reason, message string) error { +func (r *DNSZoneTSIGKeyPowerDNSReconciler) setProgrammedCondition(ctx context.Context, tk *dnsv1alpha1.DNSZoneTSIGKey, status metav1.ConditionStatus, reason, message string) error { base := tk.DeepCopy() cond := metav1.Condition{ Type: CondProgrammed, @@ -303,7 +341,7 @@ func (r *TSIGKeyPowerDNSReconciler) setProgrammedCondition(ctx context.Context, return r.Status().Patch(ctx, tk, client.MergeFrom(base)) } -func (r *TSIGKeyPowerDNSReconciler) SetupWithManager(mgr ctrl.Manager) error { +func (r *DNSZoneTSIGKeyPowerDNSReconciler) SetupWithManager(mgr ctrl.Manager) error { // Initialize PDNS client once at setup-time (unless injected, e.g. tests). // This fails fast on bad env/config rather than failing on the first reconcile. if r.PDNS == nil { @@ -314,23 +352,23 @@ func (r *TSIGKeyPowerDNSReconciler) SetupWithManager(mgr ctrl.Manager) error { r.PDNS = cli } - // index TSIGKey by dnsZoneRef for quick fan-out from a DNSZone event + // index DNSZoneTSIGKey by dnsZoneRef for quick fan-out from a DNSZone event if err := mgr.GetFieldIndexer().IndexField(context.Background(), - &dnsv1alpha1.TSIGKey{}, "spec.DNSZoneRef.Name", + &dnsv1alpha1.DNSZoneTSIGKey{}, "spec.DNSZoneRef.Name", func(obj client.Object) []string { - tk := obj.(*dnsv1alpha1.TSIGKey) + tk := obj.(*dnsv1alpha1.DNSZoneTSIGKey) return []string{tk.Spec.DNSZoneRef.Name} }, ); err != nil { return err } - // Index TSIGKey by the effective secret name stored in status.secretName. + // Index DNSZoneTSIGKey by the effective secret name stored in status.secretName. // This is what the controller actually consumes (BYO secretRef or generated). if err := mgr.GetFieldIndexer().IndexField(context.Background(), - &dnsv1alpha1.TSIGKey{}, "status.secretName", + &dnsv1alpha1.DNSZoneTSIGKey{}, "status.secretName", func(obj client.Object) []string { - tk := obj.(*dnsv1alpha1.TSIGKey) + tk := obj.(*dnsv1alpha1.DNSZoneTSIGKey) if tk.Status.SecretName != "" { return []string{tk.Status.SecretName} } @@ -341,13 +379,13 @@ func (r *TSIGKeyPowerDNSReconciler) SetupWithManager(mgr ctrl.Manager) error { } return ctrl.NewControllerManagedBy(mgr). - For(&dnsv1alpha1.TSIGKey{}). - // When a DNSZone changes, enqueue its TSIGKeys. + For(&dnsv1alpha1.DNSZoneTSIGKey{}). + // When a DNSZone changes, enqueue its DNSZoneTSIGKeys. Watches( &dnsv1alpha1.DNSZone{}, handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []ctrl.Request { zone := obj.(*dnsv1alpha1.DNSZone) - var tks dnsv1alpha1.TSIGKeyList + var tks dnsv1alpha1.DNSZoneTSIGKeyList _ = mgr.GetClient().List(ctx, &tks, client.InNamespace(zone.Namespace), client.MatchingFields{"spec.DNSZoneRef.Name": zone.Name}) out := make([]ctrl.Request, 0, len(tks.Items)) for i := range tks.Items { @@ -356,12 +394,12 @@ func (r *TSIGKeyPowerDNSReconciler) SetupWithManager(mgr ctrl.Manager) error { return out }), ). - // When a BYO secret changes, enqueue the TSIGKeys referencing it. + // When a BYO secret changes, enqueue the DNSZoneTSIGKeys referencing it. Watches( &corev1.Secret{}, handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []ctrl.Request { sec := obj.(*corev1.Secret) - var tks dnsv1alpha1.TSIGKeyList + var tks dnsv1alpha1.DNSZoneTSIGKeyList _ = mgr.GetClient().List(ctx, &tks, client.InNamespace(sec.Namespace), client.MatchingFields{"status.secretName": sec.Name}) out := make([]ctrl.Request, 0, len(tks.Items)) for i := range tks.Items { @@ -370,6 +408,6 @@ func (r *TSIGKeyPowerDNSReconciler) SetupWithManager(mgr ctrl.Manager) error { return out }), ). - Named("tsigkey-powerdns"). + Named("dnszonetsigkey-powerdns"). Complete(r) } diff --git a/internal/controller/tsigkey_powerdns_controller_test.go b/internal/controller/dnszonetsigkey_powerdns_controller_test.go similarity index 69% rename from internal/controller/tsigkey_powerdns_controller_test.go rename to internal/controller/dnszonetsigkey_powerdns_controller_test.go index 58b6b46..2dc830d 100644 --- a/internal/controller/tsigkey_powerdns_controller_test.go +++ b/internal/controller/dnszonetsigkey_powerdns_controller_test.go @@ -16,20 +16,19 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/fake" ) -type fakeTSIGPDNS struct { +type fakeDNSZoneTSIGPDNS struct { ensureCalls []struct { Name string Algorithm string Key string } - deleteByIDCalls []string - deleteByNameCalls []string + deleteByIDCalls []string ensureResp pdnsclient.TSIGKey ensureErr error } -func (f *fakeTSIGPDNS) EnsureTSIGKey(_ context.Context, name, algorithm, keyMaterial string) (pdnsclient.TSIGKey, error) { +func (f *fakeDNSZoneTSIGPDNS) EnsureTSIGKey(_ context.Context, name, algorithm, keyMaterial string) (pdnsclient.TSIGKey, error) { f.ensureCalls = append(f.ensureCalls, struct { Name string Algorithm string @@ -37,16 +36,12 @@ func (f *fakeTSIGPDNS) EnsureTSIGKey(_ context.Context, name, algorithm, keyMate }{name, algorithm, keyMaterial}) return f.ensureResp, f.ensureErr } -func (f *fakeTSIGPDNS) DeleteTSIGKey(_ context.Context, id string) error { +func (f *fakeDNSZoneTSIGPDNS) DeleteTSIGKey(_ context.Context, id string) error { f.deleteByIDCalls = append(f.deleteByIDCalls, id) return nil } -func (f *fakeTSIGPDNS) DeleteTSIGKeyByName(_ context.Context, name string) error { - f.deleteByNameCalls = append(f.deleteByNameCalls, name) - return nil -} -func newDNSOnlyScheme(t *testing.T) *runtime.Scheme { +func newDNSOnlySchemeTSIG(t *testing.T) *runtime.Scheme { t.Helper() s := runtime.NewScheme() if err := dnsv1alpha1.AddToScheme(s); err != nil { @@ -58,10 +53,10 @@ func newDNSOnlyScheme(t *testing.T) *runtime.Scheme { return s } -func TestTSIGKeyPowerDNS_ByoSecret_ValidatesAndPrograms(t *testing.T) { +func TestDNSZoneTSIGKeyPowerDNS_ByoSecret_ValidatesAndPrograms(t *testing.T) { t.Parallel() - scheme := newDNSOnlyScheme(t) + scheme := newDNSOnlySchemeTSIG(t) zone, zc := newZoneAndClass("example-com") secret := &corev1.Secret{ @@ -72,24 +67,25 @@ func TestTSIGKeyPowerDNS_ByoSecret_ValidatesAndPrograms(t *testing.T) { }, } - tk := &dnsv1alpha1.TSIGKey{ + tk := &dnsv1alpha1.DNSZoneTSIGKey{ ObjectMeta: metav1.ObjectMeta{Name: "xfr", Namespace: ns}, - Spec: dnsv1alpha1.TSIGKeySpec{ + Spec: dnsv1alpha1.DNSZoneTSIGKeySpec{ DNSZoneRef: corev1.LocalObjectReference{Name: zone.Name}, - KeyName: "datum-example-com-xfr", + KeyName: "xfr", Algorithm: dnsv1alpha1.TSIGAlgorithmHMACSHA256, SecretRef: &corev1.LocalObjectReference{Name: secret.Name}, }, } - pdns := &fakeTSIGPDNS{ensureResp: pdnsclient.TSIGKey{ID: "pdns-id", Name: tk.Spec.KeyName, Algorithm: "hmac-sha256"}} + wantPDNSName := "xfr.example.com" + pdns := &fakeDNSZoneTSIGPDNS{ensureResp: pdnsclient.TSIGKey{ID: wantPDNSName, Name: wantPDNSName, Algorithm: "hmac-sha256"}} c := fake.NewClientBuilder(). WithScheme(scheme). - WithStatusSubresource(&dnsv1alpha1.TSIGKey{}). + WithStatusSubresource(&dnsv1alpha1.DNSZoneTSIGKey{}). WithObjects(zone, zc, secret, tk). Build() - r := &controller.TSIGKeyPowerDNSReconciler{Client: c, Scheme: scheme, PDNS: pdns} + r := &controller.DNSZoneTSIGKeyPowerDNSReconciler{Client: c, Scheme: scheme, PDNS: pdns} // Reconcile is multi-step (finalizer, ownerrefs, etc). Run a few times to converge. for i := 0; i < 5; i++ { _, err := r.Reconcile(context.Background(), ctrl.Request{NamespacedName: client.ObjectKeyFromObject(tk)}) @@ -98,7 +94,7 @@ func TestTSIGKeyPowerDNS_ByoSecret_ValidatesAndPrograms(t *testing.T) { } } - var got dnsv1alpha1.TSIGKey + var got dnsv1alpha1.DNSZoneTSIGKey if err := c.Get(context.Background(), client.ObjectKeyFromObject(tk), &got); err != nil { t.Fatalf("get: %v", err) } @@ -109,8 +105,8 @@ func TestTSIGKeyPowerDNS_ByoSecret_ValidatesAndPrograms(t *testing.T) { if cond := apimeta.FindStatusCondition(got.Status.Conditions, controller.CondProgrammed); cond == nil || cond.Status != metav1.ConditionTrue { t.Fatalf("Programmed not true: %#v", got.Status.Conditions) } - if got.Status.TSIGKeyID != "pdns-id" { - t.Fatalf("expected tsigKeyID=pdns-id, got %q", got.Status.TSIGKeyID) + if got.Status.TSIGKeyName != wantPDNSName { + t.Fatalf("expected tsigKeyName=%q, got %q", wantPDNSName, got.Status.TSIGKeyName) } if got.Status.SecretName != secret.Name { t.Fatalf("expected secretName=%q, got %q", secret.Name, got.Status.SecretName) @@ -118,25 +114,28 @@ func TestTSIGKeyPowerDNS_ByoSecret_ValidatesAndPrograms(t *testing.T) { if len(pdns.ensureCalls) < 1 { t.Fatalf("expected at least 1 ensure call, got %d", len(pdns.ensureCalls)) } + if gotCall := pdns.ensureCalls[0].Name; gotCall != wantPDNSName { + t.Fatalf("expected EnsureTSIGKey name=%q, got %q", wantPDNSName, gotCall) + } } -func TestTSIGKeyPowerDNS_ByoSecret_InvalidSchemaRejected(t *testing.T) { +func TestDNSZoneTSIGKeyPowerDNS_ByoSecret_InvalidSchemaRejected(t *testing.T) { t.Parallel() - scheme := newDNSOnlyScheme(t) + scheme := newDNSOnlySchemeTSIG(t) zone, zc := newZoneAndClass("example-com") secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{Name: "byo", Namespace: ns}, Type: corev1.SecretTypeOpaque, - Data: map[string][]byte{ + Data: map[string][]byte{ // missing required secret key }, } - tk := &dnsv1alpha1.TSIGKey{ + tk := &dnsv1alpha1.DNSZoneTSIGKey{ ObjectMeta: metav1.ObjectMeta{Name: "xfr", Namespace: ns}, - Spec: dnsv1alpha1.TSIGKeySpec{ + Spec: dnsv1alpha1.DNSZoneTSIGKeySpec{ DNSZoneRef: corev1.LocalObjectReference{Name: zone.Name}, KeyName: "datum-example-com-xfr", Algorithm: dnsv1alpha1.TSIGAlgorithmHMACSHA256, @@ -144,14 +143,14 @@ func TestTSIGKeyPowerDNS_ByoSecret_InvalidSchemaRejected(t *testing.T) { }, } - pdns := &fakeTSIGPDNS{ensureResp: pdnsclient.TSIGKey{ID: "pdns-id"}} + pdns := &fakeDNSZoneTSIGPDNS{ensureResp: pdnsclient.TSIGKey{ID: "pdns-id"}} c := fake.NewClientBuilder(). WithScheme(scheme). - WithStatusSubresource(&dnsv1alpha1.TSIGKey{}). + WithStatusSubresource(&dnsv1alpha1.DNSZoneTSIGKey{}). WithObjects(zone, zc, secret, tk). Build() - r := &controller.TSIGKeyPowerDNSReconciler{Client: c, Scheme: scheme, PDNS: pdns} + r := &controller.DNSZoneTSIGKeyPowerDNSReconciler{Client: c, Scheme: scheme, PDNS: pdns} for i := 0; i < 5; i++ { _, err := r.Reconcile(context.Background(), ctrl.Request{NamespacedName: client.ObjectKeyFromObject(tk)}) if err != nil { @@ -159,7 +158,7 @@ func TestTSIGKeyPowerDNS_ByoSecret_InvalidSchemaRejected(t *testing.T) { } } - var got dnsv1alpha1.TSIGKey + var got dnsv1alpha1.DNSZoneTSIGKey _ = c.Get(context.Background(), client.ObjectKeyFromObject(tk), &got) cond := apimeta.FindStatusCondition(got.Status.Conditions, controller.CondAccepted) if cond == nil || cond.Status != metav1.ConditionFalse || cond.Reason != controller.ReasonInvalidSecret { @@ -170,17 +169,17 @@ func TestTSIGKeyPowerDNS_ByoSecret_InvalidSchemaRejected(t *testing.T) { } } -func TestTSIGKeyPowerDNS_GeneratesSecretAndPrograms(t *testing.T) { +func TestDNSZoneTSIGKeyPowerDNS_GeneratesSecretAndPrograms(t *testing.T) { t.Parallel() - scheme := newDNSOnlyScheme(t) + scheme := newDNSOnlySchemeTSIG(t) zone, zc := newZoneAndClass("example-com") - tk := &dnsv1alpha1.TSIGKey{ + tk := &dnsv1alpha1.DNSZoneTSIGKey{ ObjectMeta: metav1.ObjectMeta{Name: "xfr", Namespace: ns}, - Spec: dnsv1alpha1.TSIGKeySpec{ + Spec: dnsv1alpha1.DNSZoneTSIGKeySpec{ DNSZoneRef: corev1.LocalObjectReference{Name: zone.Name}, - KeyName: "datum-example-com-xfr", + KeyName: "xfr", Algorithm: dnsv1alpha1.TSIGAlgorithmHMACSHA256, // SecretRef omitted => generated }, @@ -196,14 +195,15 @@ func TestTSIGKeyPowerDNS_GeneratesSecretAndPrograms(t *testing.T) { }, } - pdns := &fakeTSIGPDNS{ensureResp: pdnsclient.TSIGKey{ID: "pdns-id", Name: tk.Spec.KeyName, Algorithm: "hmac-sha256"}} + wantPDNSName := "xfr.example.com" + pdns := &fakeDNSZoneTSIGPDNS{ensureResp: pdnsclient.TSIGKey{ID: wantPDNSName, Name: wantPDNSName, Algorithm: "hmac-sha256"}} c := fake.NewClientBuilder(). WithScheme(scheme). - WithStatusSubresource(&dnsv1alpha1.TSIGKey{}). + WithStatusSubresource(&dnsv1alpha1.DNSZoneTSIGKey{}). WithObjects(zone, zc, tk, secret). Build() - r := &controller.TSIGKeyPowerDNSReconciler{Client: c, Scheme: scheme, PDNS: pdns} + r := &controller.DNSZoneTSIGKeyPowerDNSReconciler{Client: c, Scheme: scheme, PDNS: pdns} for i := 0; i < 5; i++ { _, err := r.Reconcile(context.Background(), ctrl.Request{NamespacedName: client.ObjectKeyFromObject(tk)}) if err != nil { @@ -211,7 +211,7 @@ func TestTSIGKeyPowerDNS_GeneratesSecretAndPrograms(t *testing.T) { } } - var got dnsv1alpha1.TSIGKey + var got dnsv1alpha1.DNSZoneTSIGKey if err := c.Get(context.Background(), client.ObjectKeyFromObject(tk), &got); err != nil { t.Fatalf("get: %v", err) } @@ -221,6 +221,7 @@ func TestTSIGKeyPowerDNS_GeneratesSecretAndPrograms(t *testing.T) { if len(pdns.ensureCalls) < 1 { t.Fatalf("expected PDNS ensure called") } + if gotCall := pdns.ensureCalls[0].Name; gotCall != wantPDNSName { + t.Fatalf("expected EnsureTSIGKey name=%q, got %q", wantPDNSName, gotCall) + } } - - diff --git a/internal/controller/tsigkey_replicator_controller.go b/internal/controller/dnszonetsigkey_replicator_controller.go similarity index 67% rename from internal/controller/tsigkey_replicator_controller.go rename to internal/controller/dnszonetsigkey_replicator_controller.go index 8f44218..88fd85d 100644 --- a/internal/controller/tsigkey_replicator_controller.go +++ b/internal/controller/dnszonetsigkey_replicator_controller.go @@ -6,7 +6,6 @@ import ( "context" "crypto/rand" "fmt" - "strings" "k8s.io/apimachinery/pkg/api/equality" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -28,21 +27,21 @@ import ( corev1 "k8s.io/api/core/v1" ) -const tsigKeyFinalizer = "dns.networking.miloapis.com/finalize-tsigkey" +const dnsZoneTSIGKeyFinalizer = "dns.networking.miloapis.com/finalize-dnszonetsigkey" -// TSIGKeyReplicator mirrors TSIGKey resources into the downstream cluster and reflects downstream status back upstream. -type TSIGKeyReplicator struct { +// DNSZoneTSIGKeyReplicator mirrors DNSZoneTSIGKey resources into the downstream cluster and reflects downstream status back upstream. +type DNSZoneTSIGKeyReplicator struct { DownstreamClient client.Client mgr mcmanager.Manager } -// +kubebuilder:rbac:groups=dns.networking.miloapis.com,resources=tsigkeys,verbs=get;list;watch;update;patch -// +kubebuilder:rbac:groups=dns.networking.miloapis.com,resources=tsigkeys/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=dns.networking.miloapis.com,resources=tsigkeys/finalizers,verbs=update +// +kubebuilder:rbac:groups=dns.networking.miloapis.com,resources=dnszonetsigkeys,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=dns.networking.miloapis.com,resources=dnszonetsigkeys/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=dns.networking.miloapis.com,resources=dnszonetsigkeys/finalizers,verbs=update // +kubebuilder:rbac:groups=dns.networking.miloapis.com,resources=dnszones,verbs=get;list;watch -func (r *TSIGKeyReplicator) Reconcile(ctx context.Context, req mcreconcile.Request) (ctrl.Result, error) { +func (r *DNSZoneTSIGKeyReplicator) Reconcile(ctx context.Context, req mcreconcile.Request) (ctrl.Result, error) { lg := log.FromContext(ctx).WithValues("cluster", req.ClusterName, "namespace", req.Namespace, "name", req.Name) ctx = log.IntoContext(ctx, lg) lg.Info("reconcile start") @@ -52,7 +51,7 @@ func (r *TSIGKeyReplicator) Reconcile(ctx context.Context, req mcreconcile.Reque return ctrl.Result{}, err } - var upstream dnsv1alpha1.TSIGKey + var upstream dnsv1alpha1.DNSZoneTSIGKey if err := upstreamCluster.GetClient().Get(ctx, req.NamespacedName, &upstream); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } @@ -60,9 +59,9 @@ func (r *TSIGKeyReplicator) Reconcile(ctx context.Context, req mcreconcile.Reque strategy := downstreamclient.NewMappedNamespaceResourceStrategy(req.ClusterName, upstreamCluster.GetClient(), r.DownstreamClient) // Ensure upstream finalizer (non-deletion path) - if upstream.DeletionTimestamp.IsZero() && !controllerutil.ContainsFinalizer(&upstream, tsigKeyFinalizer) { + if upstream.DeletionTimestamp.IsZero() && !controllerutil.ContainsFinalizer(&upstream, dnsZoneTSIGKeyFinalizer) { base := upstream.DeepCopy() - controllerutil.AddFinalizer(&upstream, tsigKeyFinalizer) + controllerutil.AddFinalizer(&upstream, dnsZoneTSIGKeyFinalizer) if err := upstreamCluster.GetClient().Patch(ctx, &upstream, client.MergeFrom(base)); err != nil { return ctrl.Result{}, err } @@ -121,8 +120,8 @@ func (r *TSIGKeyReplicator) Reconcile(ctx context.Context, req mcreconcile.Reque return ctrl.Result{}, nil } - // Ensure downstream shadow TSIGKey mirrors upstream spec. - if _, err := r.ensureDownstreamTSIGKey(ctx, req.ClusterName, strategy, &upstream); err != nil { + // Ensure downstream shadow DNSZoneTSIGKey mirrors upstream spec. + if _, err := r.ensureDownstreamDNSZoneTSIGKey(ctx, req.ClusterName, strategy, &upstream); err != nil { return ctrl.Result{}, err } @@ -136,7 +135,7 @@ func (r *TSIGKeyReplicator) Reconcile(ctx context.Context, req mcreconcile.Reque if mdErr != nil { return ctrl.Result{}, mdErr } - var shadow dnsv1alpha1.TSIGKey + var shadow dnsv1alpha1.DNSZoneTSIGKey if err := r.DownstreamClient.Get(ctx, types.NamespacedName{Namespace: md.Namespace, Name: md.Name}, &shadow); err != nil { return ctrl.Result{}, err } @@ -148,44 +147,65 @@ func (r *TSIGKeyReplicator) Reconcile(ctx context.Context, req mcreconcile.Reque return ctrl.Result{}, nil } -func (r *TSIGKeyReplicator) handleDeletion(ctx context.Context, c client.Client, strategy downstreamclient.ResourceStrategy, upstream *dnsv1alpha1.TSIGKey) (done bool, err error) { - if !controllerutil.ContainsFinalizer(upstream, tsigKeyFinalizer) { +func (r *DNSZoneTSIGKeyReplicator) handleDeletion(ctx context.Context, c client.Client, strategy downstreamclient.ResourceStrategy, upstream *dnsv1alpha1.DNSZoneTSIGKey) (done bool, err error) { + if !controllerutil.ContainsFinalizer(upstream, dnsZoneTSIGKeyFinalizer) { return true, nil } + // Best-effort explicit deletes. + secretName := upstream.Name + generatedSecret := true + if upstream.Spec.SecretRef != nil && upstream.Spec.SecretRef.Name != "" { + generatedSecret = false + secretName = upstream.Spec.SecretRef.Name + } + md, err := strategy.ObjectMetaFromUpstreamObject(ctx, upstream) if err != nil { return false, err } - var shadow dnsv1alpha1.TSIGKey + + // Only delete the Secret in generated-secret mode. In BYO mode, multiple + // DNSZoneTSIGKeys can reference the same Secret name, so deleting it here + // would break other keys. + if generatedSecret { + var secret corev1.Secret + secret.SetNamespace(md.Namespace) + secret.SetName(secretName) + if err := r.DownstreamClient.Delete(ctx, &secret); err != nil && !apierrors.IsNotFound(err) { + return false, err + } + } + + // Then delete the downstream shadow. + var shadow dnsv1alpha1.DNSZoneTSIGKey shadow.SetNamespace(md.Namespace) shadow.SetName(md.Name) if err := r.DownstreamClient.Delete(ctx, &shadow); err != nil && !apierrors.IsNotFound(err) { return false, err } - // Ensure it's gone before removing finalizer. - if err := r.DownstreamClient.Get(ctx, types.NamespacedName{Namespace: md.Namespace, Name: md.Name}, &shadow); err == nil { - return false, nil - } else if !apierrors.IsNotFound(err) { + // Finally delete the anchor ConfigMap for this upstream object. This drives GC for + // downstream artifacts (shadow + replicated Secret) that are owned via the anchor. + if err := strategy.DeleteAnchorForObject(ctx, upstream); err != nil { return false, err } base := upstream.DeepCopy() - controllerutil.RemoveFinalizer(upstream, tsigKeyFinalizer) + controllerutil.RemoveFinalizer(upstream, dnsZoneTSIGKeyFinalizer) if err := c.Patch(ctx, upstream, client.MergeFrom(base)); err != nil { return false, err } return true, nil } -func (r *TSIGKeyReplicator) ensureDownstreamTSIGKey(ctx context.Context, upstreamClusterName string, strategy downstreamclient.ResourceStrategy, upstream *dnsv1alpha1.TSIGKey) (controllerutil.OperationResult, error) { +func (r *DNSZoneTSIGKeyReplicator) ensureDownstreamDNSZoneTSIGKey(ctx context.Context, upstreamClusterName string, strategy downstreamclient.ResourceStrategy, upstream *dnsv1alpha1.DNSZoneTSIGKey) (controllerutil.OperationResult, error) { md, err := strategy.ObjectMetaFromUpstreamObject(ctx, upstream) if err != nil { return controllerutil.OperationResultNone, err } - shadow := dnsv1alpha1.TSIGKey{} + shadow := dnsv1alpha1.DNSZoneTSIGKey{} shadow.SetNamespace(md.Namespace) shadow.SetName(md.Name) @@ -200,17 +220,17 @@ func (r *TSIGKeyReplicator) ensureDownstreamTSIGKey(ctx context.Context, upstrea } // Set owner reference using the mapped-namespace strategy (anchor-based). - // NOTE: We intentionally do not manage anchor deletion here. + // Anchor deletion is handled in the upstream deletion path (handleDeletion). return strategy.SetControllerReference(ctx, upstream, &shadow) }) if cErr != nil { return res, cErr } - log.FromContext(ctx).Info("ensured downstream TSIGKey", "operation", res, "namespace", shadow.Namespace, "name", shadow.Name) + log.FromContext(ctx).Info("ensured downstream DNSZoneTSIGKey", "operation", res, "namespace", shadow.Namespace, "name", shadow.Name) return res, nil } -func (r *TSIGKeyReplicator) updateStatus(ctx context.Context, c client.Client, upstream *dnsv1alpha1.TSIGKey, downstreamStatus *dnsv1alpha1.TSIGKeyStatus) error { +func (r *DNSZoneTSIGKeyReplicator) updateStatus(ctx context.Context, c client.Client, upstream *dnsv1alpha1.DNSZoneTSIGKey, downstreamStatus *dnsv1alpha1.DNSZoneTSIGKeyStatus) error { if downstreamStatus == nil { return nil } @@ -222,7 +242,7 @@ func (r *TSIGKeyReplicator) updateStatus(ctx context.Context, c client.Client, u return c.Status().Patch(ctx, upstream, client.MergeFrom(base)) } -func (r *TSIGKeyReplicator) ensureSecretReplication(ctx context.Context, upstreamClusterName string, upstreamClient client.Client, strategy downstreamclient.ResourceStrategy, upstream *dnsv1alpha1.TSIGKey) error { +func (r *DNSZoneTSIGKeyReplicator) ensureSecretReplication(ctx context.Context, upstreamClusterName string, upstreamClient client.Client, strategy downstreamclient.ResourceStrategy, upstream *dnsv1alpha1.DNSZoneTSIGKey) error { // Determine the source secret name. secretName := upstream.Name if upstream.Spec.SecretRef != nil && upstream.Spec.SecretRef.Name != "" { @@ -258,7 +278,7 @@ func (r *TSIGKeyReplicator) ensureSecretReplication(ctx context.Context, upstrea src.Data = map[string][]byte{} } if len(src.Data["secret"]) == 0 { - raw := make([]byte, 32) + raw := make([]byte, tsigKeySecretLen(alg)) if _, err := rand.Read(raw); err != nil { return err } @@ -279,9 +299,9 @@ func (r *TSIGKeyReplicator) ensureSecretReplication(ctx context.Context, upstrea } dsClient := strategy.GetClient() // ensures downstream namespace exists on Create - // Fetch downstream TSIGKey shadow for owner reference (GC in downstream). - var shadow dnsv1alpha1.TSIGKey - if err := r.DownstreamClient.Get(ctx, types.NamespacedName{Namespace: md.Namespace, Name: upstream.Name}, &shadow); err != nil { + // Fetch downstream DNSZoneTSIGKey shadow for owner reference (GC in downstream). + var shadow dnsv1alpha1.DNSZoneTSIGKey + if err := r.DownstreamClient.Get(ctx, types.NamespacedName{Namespace: md.Namespace, Name: md.Name}, &shadow); err != nil { return err } @@ -296,51 +316,38 @@ func (r *TSIGKeyReplicator) ensureSecretReplication(ctx context.Context, upstrea // Copy secret bytes exactly. dst.Data["secret"] = append([]byte(nil), src.Data["secret"]...) - // GC: owned by downstream TSIGKey shadow. - if err := controllerutil.SetControllerReference(&shadow, dst, dsClient.Scheme()); err != nil { - // If the secret is already controlled by something else, leave it and error to surface issue. + // Set owner reference using the mapped-namespace strategy (anchor-based). + // Anchor deletion is handled in the upstream deletion path (handleDeletion). + if err := strategy.SetControllerReference(ctx, upstream, dst); err != nil { return err } - // Stamp upstream-owner labels WITHOUT adding the anchor ConfigMap ownerRef. - // The Secret must be GC'd when the downstream TSIGKey shadow is deleted. - labels := dst.GetLabels() - if labels == nil { - labels = map[string]string{} - } - labels[downstreamclient.UpstreamOwnerClusterNameLabel] = fmt.Sprintf("cluster-%s", strings.ReplaceAll(upstreamClusterName, "/", "_")) - labels[downstreamclient.UpstreamOwnerGroupLabel] = dnsv1alpha1.GroupVersion.Group - labels[downstreamclient.UpstreamOwnerKindLabel] = "TSIGKey" - labels[downstreamclient.UpstreamOwnerNameLabel] = upstream.Name - labels[downstreamclient.UpstreamOwnerNamespaceLabel] = upstream.Namespace - dst.SetLabels(labels) - return nil }) return err } -func (r *TSIGKeyReplicator) SetupWithManager(mgr mcmanager.Manager, downstreamCl cluster.Cluster) error { +func (r *DNSZoneTSIGKeyReplicator) SetupWithManager(mgr mcmanager.Manager, downstreamCl cluster.Cluster) error { r.mgr = mgr b := builder.ControllerManagedBy(mgr) - b = b.For(&dnsv1alpha1.TSIGKey{}) + b = b.For(&dnsv1alpha1.DNSZoneTSIGKey{}) src := mcsource.TypedKind( - &dnsv1alpha1.TSIGKey{}, - downstreamclient.TypedEnqueueRequestForUpstreamOwner[*dnsv1alpha1.TSIGKey](&dnsv1alpha1.TSIGKey{}), + &dnsv1alpha1.DNSZoneTSIGKey{}, + downstreamclient.TypedEnqueueRequestForUpstreamOwner[*dnsv1alpha1.DNSZoneTSIGKey](&dnsv1alpha1.DNSZoneTSIGKey{}), ) clusterSrc, err := src.ForCluster("", downstreamCl) if err != nil { - return fmt.Errorf("failed to build downstream watch for %s: %w", dnsv1alpha1.GroupVersion.WithKind("TSIGKey").String(), err) + return fmt.Errorf("failed to build downstream watch for %s: %w", dnsv1alpha1.GroupVersion.WithKind("DNSZoneTSIGKey").String(), err) } b = b.WatchesRawSource(clusterSrc) - // Also watch downstream Secrets (generated or BYO replicated) to ensure upstream TSIGKey reconcile + // Also watch downstream Secrets (generated or BYO replicated) to ensure upstream DNSZoneTSIGKey reconcile // happens when secret material changes (or is first created). secretSrc := mcsource.TypedKind( &corev1.Secret{}, - downstreamclient.TypedEnqueueRequestForUpstreamOwner[*corev1.Secret](&dnsv1alpha1.TSIGKey{}), + downstreamclient.TypedEnqueueRequestForUpstreamOwner[*corev1.Secret](&dnsv1alpha1.DNSZoneTSIGKey{}), ) secretClusterSrc, err := secretSrc.ForCluster("", downstreamCl) if err != nil { @@ -348,5 +355,27 @@ func (r *TSIGKeyReplicator) SetupWithManager(mgr mcmanager.Manager, downstreamCl } b = b.WatchesRawSource(secretClusterSrc) - return b.Named("tsigkey-replicator").Complete(r) + return b.Named("dnszonetsigkey-replicator").Complete(r) +} + +func tsigKeySecretLen(alg dnsv1alpha1.TSIGAlgorithm) int { + // Align with HMAC guidance: key length == hash output size. + // (RFC 2845 for TSIG; RFC 4635 adds the SHA2-based TSIG algorithms.) + switch alg { + case dnsv1alpha1.TSIGAlgorithmHMACMD5: + return 16 + case dnsv1alpha1.TSIGAlgorithmHMACSHA1: + return 20 + case dnsv1alpha1.TSIGAlgorithmHMACSHA224: + return 28 + case dnsv1alpha1.TSIGAlgorithmHMACSHA256: + return 32 + case dnsv1alpha1.TSIGAlgorithmHMACSHA384: + return 48 + case dnsv1alpha1.TSIGAlgorithmHMACSHA512: + return 64 + default: + // Unknown/empty algorithm: keep existing behavior (safe default). + return 32 + } } diff --git a/internal/controller/dnszonetsigkey_replicator_controller_test.go b/internal/controller/dnszonetsigkey_replicator_controller_test.go new file mode 100644 index 0000000..6e7343c --- /dev/null +++ b/internal/controller/dnszonetsigkey_replicator_controller_test.go @@ -0,0 +1,34 @@ +package controller + +import ( + "testing" + + dnsv1alpha1 "go.miloapis.com/dns-operator/api/v1alpha1" +) + +func TestTsigKeySecretLen_EqualsHashOutputSize(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + alg dnsv1alpha1.TSIGAlgorithm + want int + }{ + {"hmac-md5", dnsv1alpha1.TSIGAlgorithmHMACMD5, 16}, + {"hmac-sha1", dnsv1alpha1.TSIGAlgorithmHMACSHA1, 20}, + {"hmac-sha224", dnsv1alpha1.TSIGAlgorithmHMACSHA224, 28}, + {"hmac-sha256", dnsv1alpha1.TSIGAlgorithmHMACSHA256, 32}, + {"hmac-sha384", dnsv1alpha1.TSIGAlgorithmHMACSHA384, 48}, + {"hmac-sha512", dnsv1alpha1.TSIGAlgorithmHMACSHA512, 64}, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + if got := tsigKeySecretLen(tc.alg); got != tc.want { + t.Fatalf("tsigKeySecretLen(%q)=%d, want %d", tc.alg, got, tc.want) + } + }) + } +} diff --git a/test/e2e/kubeconfig-downstream b/test/e2e/kubeconfig-downstream index 6af829e..99dd881 100644 --- a/test/e2e/kubeconfig-downstream +++ b/test/e2e/kubeconfig-downstream @@ -1,8 +1,8 @@ apiVersion: v1 clusters: - cluster: - certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCVENDQWUyZ0F3SUJBZ0lJSkVxdDlJUTRvNkl3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TmpBeE1USXlNRE0wTVRoYUZ3MHpOakF4TVRBeU1ETTVNVGhhTUJVeApFekFSQmdOVkJBTVRDbXQxWW1WeWJtVjBaWE13Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLCkFvSUJBUUNuWGpVQkR6cTRpenpiNlVUdndPSFpoN1hhaEJpZm5aZ0pXdDN2VUIreTUyZzRhOFovTVozSnlBZ2QKUlZJZk1iVEJFY2JtdllEb08wOTBjaXA3M3RHTFBFczJYSnMwZVFPVmNrVzRSUHlUb2g5djJwcjFKUSthcDROcwpZYms1ZUJLVjdyb3lFSVpRbEJTL1plWVgvaFFCOXpZMHIwZEVDQUJSL3FRZWZ6MjZUVjhCV2c3dnpDZDU0cXlLCktmZGRjZlRreFZGeVVDRGI3SHpYV28zOE1iMThWeVc5OXgyNjhWcUY3RmRiUkdlaUN1UmlLYmIwZUJTRVVBYkwKY3JKeDJPR3VXSU5FamlabTRsM1IxcFB1S1hHRGJyTU5oK0VkdHYrOFdVeFdFdFFzelMxT0lUUW4xUHo2UXYyZQpFckZEd2ViY1luTkFzaFlRaDE2eC9kSU54bFczQWdNQkFBR2pXVEJYTUE0R0ExVWREd0VCL3dRRUF3SUNwREFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUIwR0ExVWREZ1FXQkJRanYzRFI5TjY0Z2xPc1FQQXVjRVFkNW5vSHVqQVYKQmdOVkhSRUVEakFNZ2dwcmRXSmxjbTVsZEdWek1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQ1FhbVZCMmY1ZApuZ1FsdWY5a2JuNkZQYThjczQxUE5MVkxHT3VXMXMzbEEvUkdoZnp2RnI5MDFsTHVtYWFIR2s0TzFRNmkyNmtTCjRkd2g3cXVkMkFXMHVwWGthalVxTzhzK3FtUzlQeGxUdHIrbXJwZHg4WXNGMkdQbWlLMmxiTnNkeTVpVm50di8KQzFxMXNKV2dGbkVjOUh2QXQvQ3JSK3Ixcy9nMFc2Yk9aUVI2U3BISUd3YjE3azgvcTZUTVVaWWZlblp3bWU1KwpLODYxdW9lQXN5aWMwN2ptZUVZM0RaWUlXYWxCTWJySXd6MDUzTGJIbW1CTUpaT3IvL1ZiUzRYV0UvODRoa1F4CkdqczRrT3hudEpma0xUQlhoTTFxUlBNVjBHdkFsRXlDbFJTYnVybmI0clJLS1JTc0piTFBBbjZGVU5lcUdIcEMKSVgweGVPWXBudlRWCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K - server: https://127.0.0.1:51697 + certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCVENDQWUyZ0F3SUJBZ0lJTFJpYlNEOWVQV013RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TmpBeE1UTXhPVEkyTVRKYUZ3MHpOakF4TVRFeE9UTXhNVEphTUJVeApFekFSQmdOVkJBTVRDbXQxWW1WeWJtVjBaWE13Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLCkFvSUJBUUN2aDk3MnFPcDJTUHRCOWU3ZlRPMHVrUkVzaXkrcmtWNytqS1hxTkFmNWlTQkx1TmVVdjdOSDFSMjMKaUNEaVU2UFZxY3NRZzU1YmpmWThndmZ0OXZJVGxFZG1tbFl3NUtlYkh5cGRSY3hXb1FvY1hEbTV3aXdkdGpKTQpncEpId0VreDhOYXlJalliVk0zcDhoSjArcW1EQUREbHkxSVRpNjZmSU1Ga3R1RFM4bnhxWWRRdVhUdzZIbHhPCldoMlM4em1IZ3c4SkFTSTFNdm51Wnk1c3UvWnQ4ck9odWUwWDVXQ2VoSDI3NGtMSCtFa3V4dm16MVV3NzAxSmgKdzFRTmVSTWVmdExPQUl6K0lXemV2WHB1WXZtS2x2a0ZTV01zb3I3NTM3eHdCclh1eVdlWnFiZzdTUlFyS3gvYwpyMk1VS29UOGw0RHRBWkFRWHZObkZSamlQL1hMQWdNQkFBR2pXVEJYTUE0R0ExVWREd0VCL3dRRUF3SUNwREFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUIwR0ExVWREZ1FXQkJSSGdSWTFsRjlia2NTK3ZsS3QzZUxIcnVHSjlqQVYKQmdOVkhSRUVEakFNZ2dwcmRXSmxjbTVsZEdWek1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQjRiY3BKOEJ1RgpjTTErRVNldTNYamdSb2taSG5BeTF5WXJudXlxdUxxbXpXUWFxemdpUXp5VlhKOC9IaTZUM3RSQXFQUGN0OG1JCkdDeTZrc3VPejlOZk5kdERzaGNDWlZ6ZTh5NjBHT2ZFTkhkL01QbWFBM0YvMUpCZTIwMEtpeExENkZmODdicXoKVlZzNjl2ZjMxdG5zNU13MEZ1RVVHdncvOFRSdjVqblBXVktTTjJvMWxJUy9Delh0MzEzclZYakc2Zk9TOGsxWApLMXVkRkoxQ0FiM1dJQlNMbjRMZlJ5KzdyU0EzUExCM05iMUlXNGNCRFRZbEQ3K1ZhTkdRalR5V29pb3BnNlp6CnR1WjJ3SUhQcjk1VUJJYU53cWNxRExFakhNWGVCSnZxVkhxWE9OaXlXTHRBdHhqbjA2RVpsQW9nOWwzNUtoNXIKMUxvdEs3a0JPZFh3Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + server: https://127.0.0.1:64712 name: kind-dns-downstream contexts: - context: @@ -14,6 +14,6 @@ kind: Config users: - name: kind-dns-downstream user: - client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURLVENDQWhHZ0F3SUJBZ0lJRXFNTkJnTjFqVDB3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TmpBeE1USXlNRE0wTVRoYUZ3MHlOekF4TVRJeU1ETTVNVGhhTUR3eApIekFkQmdOVkJBb1RGbXQxWW1WaFpHMDZZMngxYzNSbGNpMWhaRzFwYm5NeEdUQVhCZ05WQkFNVEVHdDFZbVZ5CmJtVjBaWE10WVdSdGFXNHdnZ0VpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCRHdBd2dnRUtBb0lCQVFEVXI4NjEKWk5Rczl1MGpHSnBZYW9BeG1LTUVVdFIrZ2Z1aDFKSDBxK1Z2TzlpV2JSQ2ZCN2hnR1htUGVyQVFPM2tGekJleQoxTjIyWXhqQ0YwQUNVMUhNbVFpclh6OUlBMm44TCtoQW9QZlJqZ2JlZUxrNEJjL24wTU5aS3pMbStNTnY2bVFuCnlndC9ZYTFhdzN5d09rdjlPdVo0RjNiMzk2ZjBldjBwZlkvSnZKMWVvaWVCN09wUHJDaGpwcytBTzEwYUt1Ty8KemRLOHZTZE5ZN1J3TXlCY01wZ3JzQ3QyNUoyQm1kRFo0aldxUmpjL3QwVHJ2Ry9YSlZkb2NhYVJoSUtmVmg3WgpVWUVuRHZQd1BwWDJVK0JkYmVKMXRNNUpSRkJiVlpQdHNZUXgvODdHRW5QQmNpRC8xU0V4YmNMOFJtQlZHVlAyCkZaNmpkN2xhVVM1RGhuQ3JBZ01CQUFHalZqQlVNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUsKQmdnckJnRUZCUWNEQWpBTUJnTlZIUk1CQWY4RUFqQUFNQjhHQTFVZEl3UVlNQmFBRkNPL2NOSDAzcmlDVTZ4QQo4QzV3UkIzbWVnZTZNQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUIzU1lpZkZXSDZQMnQ3czJNTzFmWGNJdlJwCjVheXU3Nms2VHovYkU3ZlE5OGtJVERpVFh4NE1DVEpmVTExMEhBeFUxWXNXamczMUtPNERuNGZMZEhaL3BETHcKQWROVlFOa0s1UjlHOVl1bUZxTlh6WUw2YWh1OTVvUWNqdUNQQnRnMEdNOXNpeHZESWh2ejlaTkVNRldoSnJRRgpxNkR5M3o0THF2cjA2M1g1TzRPeHA0N0gwRm1vcTh5b2Y1aXE0d0RhTHNjOVhEbEYvdG9oSFhaTUxxbzJEN0tHCk50T2tTTnNDcy9SZXFONHBxcWdHN3FPWktoS0pDSkFiNmpiTG1tN3FSN0JZSHd4Umlqa25vZWc0NHJ6YmVITm0KMkhSZzJLZjlwSk5IV1lkSzhOV2YzUjdSN1R5QzJCb2daYUt3NWpwWXVTTXVWa2lseE1Kc1U3emQ2YmtYCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K - client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcFFJQkFBS0NBUUVBMUsvT3RXVFVMUGJ0SXhpYVdHcUFNWmlqQkZMVWZvSDdvZFNSOUt2bGJ6dllsbTBRCm53ZTRZQmw1ajNxd0VEdDVCY3dYc3RUZHRtTVl3aGRBQWxOUnpKa0lxMTgvU0FOcC9DL29RS0QzMFk0RzNuaTUKT0FYUDU5RERXU3N5NXZqRGIrcGtKOG9MZjJHdFdzTjhzRHBML1RybWVCZDI5L2VuOUhyOUtYMlB5YnlkWHFJbgpnZXpxVDZ3b1k2YlBnRHRkR2lyanY4M1N2TDBuVFdPMGNETWdYREtZSzdBcmR1U2RnWm5RMmVJMXFrWTNQN2RFCjY3eHYxeVZYYUhHbWtZU0NuMVllMlZHQkp3N3o4RDZWOWxQZ1hXM2lkYlRPU1VSUVcxV1Q3YkdFTWYvT3hoSnoKd1hJZy85VWhNVzNDL0VaZ1ZSbFQ5aFdlbzNlNVdsRXVRNFp3cXdJREFRQUJBb0lCQUF2OFNyWXp4elg4ckUrWgptdTkzemNXWFZOVTk4T2s0S1RBR3F3ZkZpYk0ybnZXWCtLTnFhNDk3b0FnVEZMQWI0M3B6d1lDSWEvNGdiUmttCktrVGRyZkxOUzFuR2p4MHNNT0UxdHJvWFBvWlRianVxZDQ4MFFYSG9Eam9tSzl5U3FxRFI3ZGI0THFUamJ3NnQKSmxSNW9Wa0lTckl2YWU0WUwrNXRGSzVncnd1dTBlRTNoeG9RYjNQMmF5TEI2aGd3RWRVS2hPcUtYSmQrM0JxbApSRHhoWDlMNGRxbU8venF1cE9TVk5iamtzTWV2ZGVKTDFycFZUU21KTWR3MEdsZjFXRjhMeFBqZVZ6R1ZxUjZyCjNYdkJkTmhLWlFoN1dZeXI0eDk5YVlCZnFpT0k5NUxDemFDdFBZQjBSejBiWWlZM3paTHJSRXlsd3FLK0Jxa04KZ0licjBhRUNnWUVBNmxtbWJrUlJMYVBwSkY2V3ZLVE1qVDNBS2tZVmIrRmdjdEt1S3JTY2RtOE5DVFJuY3d2SAphWWhvYjBHVldJR3I3cjkxNUxFTnZoa2hMWjBRcEJsVkdwMDkyazhXZ1BRTlllaHJ4cDhFeldUMTZpVjJYNVRpCmgxcjhTc04raU9VNThFa0JCQ1dOTElieGRtUWU2MTh6S1gyUlMzUUZqR3ZZS0pkUFAxeEdOa2tDZ1lFQTZGWFIKM0swRXNDTitpYmdodTE5Zlhla2xrOGJuNHZhOUZUSkdmTG5KSExEU1ErK250bkcvNEh5VkVLZGEzRnN2NWZNZQpTVUo0dDhHZnF4a0VIWCtxUzZ3VHVDUytRUnpMR0I4THFxRXB0bUdUMnJsTklCa2Z5RVNvWjRNN1VjNllkWS9sCmFDaW9ZenZMaWVDc2FtME9TeFozejljOEgzM0E2QlRSVFZhd0gxTUNnWUVBanYyc28xTmtCT2tpZEdLU3J3QVAKSDQ4eUZaazFzMUpkT3pKNXV1MEJHdktmamFKQURONS9DbEdGQjMySTFyd29ZRURLZW9QZDBzUWFqbTVyblBVbwpERmt0U0d0QlcrV04xTk93RHowdi9QTkJhV0Q2WFUvRytMZjNnTmJQK2srRGpxMjh4UDcwcU5xZHNwTmNtbGs0CktuVEhsclp3UEVJQlhxTVVZNkMxNXFFQ2dZRUFzNStGL01LWFdVWlgwa25WYW5PMTIza2hZRHJybElHR2RoakUKZmpGMDF3V3R5bkJDamI4cnhYY01HREFMQTBwTW9jOXdudHNSVWFBVXZjYzljMEQ4ZkR5eGtqQjJGd2tYeTdKVQo1cnBxOFdKSFdWYmgxZXNXczFMQmtDWFplc25xL1JrZkY0UTNpMkR6WDhtZ0F6Z0ZVUEF4K1RKQ2ZXWlAraDMrCkkzamQrWmtDZ1lFQWs5RW0zdzhOQ0tQbHkzWUlrTVN1YUVoSm82S0Zic09FN0FJZ1dHajBMS3hjUnNidVZ4VG0KSkU1ck9VVlFkcXZuR1NuSUo3TVo0TXNPcWptVklNV3NNUGNOQmNCTzAxbVZRUndxRTdVRXI4SWVKRUR4Z0laUwpOdzlTK0pOaTcyRnY2ZzkvWWtsUGZIbEhIWk5HRXdua2FnMytucDIwbTMvR2wzODNCckYzTGtzPQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo= + client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURLVENDQWhHZ0F3SUJBZ0lJSjFMN25nVngwVm93RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TmpBeE1UTXhPVEkyTVRKYUZ3MHlOekF4TVRNeE9UTXhNVEphTUR3eApIekFkQmdOVkJBb1RGbXQxWW1WaFpHMDZZMngxYzNSbGNpMWhaRzFwYm5NeEdUQVhCZ05WQkFNVEVHdDFZbVZ5CmJtVjBaWE10WVdSdGFXNHdnZ0VpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCRHdBd2dnRUtBb0lCQVFEcDFBSnQKeE9mckIyclJhemJqV1JvQ0NqYTVKNUZUd3JZelQ3WDFVMXU0MlYwWm4xZE44STlvRUkyZ1M5YXVRSzlIQzRXSQpXSktOSTZuY1p0SDRFU2ptdkc1UzBscXYrTDhBeW1CRS9vUmdBTmhSZFgzbkl4NFZJWXorQkxZY2pncXJPd2JaCkhVREZPQ20rbTZ5ZFpEQlpEdzZ3Y3ZxSDFiWjBTWkdBeFlSLyt6dXVtY2gwZFZ6N05pajIrU3Q2d0ZPSHptZDkKV1N1bkRHVUllRWxKOXNEaEdCcjl3dzN2V0R0Y3FQL2dUZVNhNWVXZlVtTUo0OUFkczNxUU1mTmVScFJPeS8zbApVT0lPbHZCMTVobXU5UG05NFNXQ3lFUzNXVVk0MDd6VWZaWk5DdzM1MUh4Y3pWR3RlUFY5YjVWUFhrL0ZKT25PCkl0OXp1MVNKSStUMk9LNnRBZ01CQUFHalZqQlVNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUsKQmdnckJnRUZCUWNEQWpBTUJnTlZIUk1CQWY4RUFqQUFNQjhHQTFVZEl3UVlNQmFBRkVlQkZqV1VYMXVSeEw2KwpVcTNkNHNldTRZbjJNQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUFheGtKTkl0bkpKYzNMY2FUQkoyV09OdXRxCnFoM0FSZUVTQkVlSGdRazMyYWdOS3cyYnJRcmpBd1lGV3p1T2xkMGFMbEo4RitEMk9WMTJ1YlZKd3A4bmFkR1AKa3ZaTVBLMXRUTlgwZ0U1cllqNFdsZG41UnZabmpJV1FDdW1IVjFDTURzeENKQVArOFJYRlVaSWp5TFJ1cGRjMgpLNEhrZldNanh3V3V6ci9qRDljeFZNaytXaXVrTEJDQjNiODNnVEllTHVGeSszeG0rMHpSejQxV0dlUkhsb29pCjBZUzRPdjdGTklaMmtqTHhWNG9WYkF6NEM3WDdDUE4yODhNVUh0Y3V6Qi9sMFpLa2gwdGtPY3VxQWYvZktyVTkKNnNTYk0zZDJRV1JHMnRMNFBtWnE4UlBGQndyRGF3UWJvSFl5eCtpUGVWS2xidzRmNG5yTkpHZGk5OXA3Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBNmRRQ2JjVG42d2RxMFdzMjQxa2FBZ28ydVNlUlU4SzJNMCsxOVZOYnVObGRHWjlYClRmQ1BhQkNOb0V2V3JrQ3ZSd3VGaUZpU2pTT3AzR2JSK0JFbzVyeHVVdEphci9pL0FNcGdSUDZFWUFEWVVYVjkKNXlNZUZTR00vZ1MySEk0S3F6c0cyUjFBeFRncHZwdXNuV1F3V1E4T3NITDZoOVcyZEVtUmdNV0VmL3M3cnBuSQpkSFZjK3pZbzl2a3Jlc0JUaDg1bmZWa3Jwd3hsQ0hoSlNmYkE0UmdhL2NNTjcxZzdYS2ovNEUza211WGxuMUpqCkNlUFFIYk42a0RIelhrYVVUc3Y5NVZEaURwYndkZVlacnZUNXZlRWxnc2hFdDFsR09OTzgxSDJXVFFzTitkUjgKWE0xUnJYajFmVytWVDE1UHhTVHB6aUxmYzd0VWlTUGs5aml1clFJREFRQUJBb0lCQUFSSVlBb1NhMjV0aUR1dAplSkV2eXhJSmpaNVU1dGZ1Zk80dVFDbG1ma2V5c0t1Y0R1N1V3SW5IL3lFREw4TVNkdENSUDNUQXJXQmQwZ2dxCmxNVEMrZERzVUFTKzZSdjFhWTdpUkNhS3BpYjdobVF0TlBhcWRTd2dCcmcrN2hTZHNTbzNJRkdTYU9MUitDc3QKYzdPUW8va2RuT1g0R09xY08xaFZOTnNLSFhNTEUwazhLTXNzcmtPRm13SitSRlMyckxBQlplbHVvQVpNUWFWUgpVQ0E5eXpCVk1lMFlualZ1Vzl0dU01a0pJMnNuNEEyRnFJZFJ4SUxVY1I4MG1sWk9nbEpURXZpalVXMlRrY3dXCjd0R0N0V1IvMVowNXhRV000NHUzY21Kb3pONXh2VDErSzFLazFNbjB0amZnZy9iRkhuaHpuQWt4Ynd3cG9tZmQKWCt5Ym1Ta0NnWUVBN1hjQ0UycDRzOGQ1RGFIaXFxRUVjNW5nY2tabjBGZ2lSUENrSmx1SnlQdGtWQkEzT2dMYwpsRWlscEtoekc2emxHTnlENFN5L1E4YUJvdHFDYkZVRExDMmZnT2FhQVhSVlhiNWdIazc4SENleUlWYU9UZkU4Cm1ZdGE5dEUvUVl5QTFqNTBjZmhOOC9TWG5YMWVTQngvSjY2cHJqWVU4RnptMWttSXBzaTdmWnNDZ1lFQS9CUlYKUXRGZWU5aGNmcVV1VnQyUEhZSGxYcEdob2xHVzZXTEx1V3Q0TkgxRXNoYzdMeVNYRS9kbHNYRWE1UjU2ZFNYQQpKUVZHV3p4NGU2MVd4Mnp4azl4SFBBdm1pUmltbnc4VnFIc3J3ZTRYTGxtU092YjdqQUdEdDNESm5Oa2Y0Q0RxCjVOdnlSdDlUUkxTV3hJR3pqM3BHWTJSbnpydW4zSEg4TUNGeWJWY0NnWUVBbXFZcnN1dGZTbTM1TjFpYm50WVkKYVJUb3FHT1R6b3JuWHBCOXh3Rk1mWmpESVVBaVIyUi90UTZPMmVwZWRNS252UVkzMlJqa1EwWnZQTmtqb1Z2SQpJaWhnUFhseENNdHpvUWFQNEkwK0FUUVUvVU02a0NZd2VpclloZStHUzdFdVl0anZ5eDJUM3ZJSEg2ajdFdW1FCklock5KTWpSNEN3UXBiUGtEQUtrb0VzQ2dZQlNkbGhaN21IcFE2TW1idVRVMTgvY2lFUy9oZ2FKTWdXYlBZMkYKajZtWUNpNnh6N1cxdTFPTTNZNnYyRjlDK3BCMnlDMnVMcWFRYkJ6QjRMZVZyNGJycHRES3pOM1NsWFRVYmJ2WgpETW9JdTlscmVUUEVCRTNQeENNUm5Gem42WU5xNzNuSCtrZXNkWndveXFiVGk5Wndwa0JtZlU4VUt3RkR0U29aCm1LZDFLd0tCZ1FDeDhIYjhPZHhpbGpHSXpPdEE5THFRRE5rY0xoVUpZb0hzNDIvUkl2TFV1ZGxibmpnSEZRYmcKQXJVOExHOGhxelQzSVNvRCszMFBQVHBDMk41am80WFFVRFBra2c0VnlwbURHMzlPZFdoUFUyRk0ya1ZpVWdScwp5MW95NTJsWk9LMmU1U1ZQb21YTUdOakdkNWRpUSt4SnBRTkpicVVMZGhsZWhIRVNxamtNemc9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo= diff --git a/test/e2e/kubeconfig-upstream b/test/e2e/kubeconfig-upstream index bd17f7a..b0a8be5 100644 --- a/test/e2e/kubeconfig-upstream +++ b/test/e2e/kubeconfig-upstream @@ -1,8 +1,8 @@ apiVersion: v1 clusters: - cluster: - certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCVENDQWUyZ0F3SUJBZ0lJUlZobTdHdWtZN3d3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TmpBeE1USXlNRE0xTURGYUZ3MHpOakF4TVRBeU1EUXdNREZhTUJVeApFekFSQmdOVkJBTVRDbXQxWW1WeWJtVjBaWE13Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLCkFvSUJBUURIdjlFNVZKcFlqSDJHRUFQQkpRN3ZYMyt0SUNlVkI4Wmo4R25FRkJoMFd4VkN4TFF1cENWQ3kvU3IKcFBTblY4MHRYWUx1WHh0TDlkZkM3K0F2SEQySVJQbC9RVi80VVVQZkJqcHRuOEFDQm9XbEdsSlIwSkFrdnVSUQp0RnpxaFZwOWNnVHZFVm5aaEs4UjJpR3g5Z0FWcUJBRHFKS05oWmlBNm1lQXNDazQ2M3JzZ09oOCtsS0NGSlM1ClZCOEJ5MGl2UzVTSHo5eHJiNWJmUUxxcDVVNUZUQ1RMeUxiaHlTTDFBNjlQZGc3TzVuSHB4MEVBbXNtdFRPUlgKL3ZZNStZeFRQYlcrRkF3aTM2ZmlRZ0FmODNiQmRLdVdUOHpPeWwwYjJiczh2Um94dEI5LzdoeW9EYVhhRGRYcQp3V1kybGxCMW1YZmQ3eE1VMUxHVlVjZnpHcVgvQWdNQkFBR2pXVEJYTUE0R0ExVWREd0VCL3dRRUF3SUNwREFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUIwR0ExVWREZ1FXQkJSVFA4Vm83N0JvcjRnQ1g3SGZ2L0FvZUFyRWNEQVYKQmdOVkhSRUVEakFNZ2dwcmRXSmxjbTVsZEdWek1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQlFYYTg1L3UwQgpBUXJGbGVtcWpDaUZmTGJLRURpcTBSaWxNRmFWWUY0QnVrK0htcUpSbHA2ditRUytMR1U1NzljOXc1V3ExVlpUCnFuOE0zbkI1S1REeXRMVGFHaHhNSkpuODFSMjA3SWtudXZRdlZaWFZVOTZtd2FSQWdvQnlQNWpBVU9FZXJtclgKaGR2bWtDSzFHeTl1OHhpa0d3dmo2aURwUmxPSzlocUVkN0lvNFZrd0xtNFgreGtoR1UwK2RFaUluNkVjczRCKwpnR3BuZkEyb2tPUWh0bis2Tnhrd3JwekpPN0N2RVJpM3N3YmMvRkU3N3lsam9kUVNaZExVa1FrR1VuVDR6dVRmCmdYbEJ1ZE5Qc1doczEvR1hFTURpcUFDckZiaEF1WHpEU2RqTkpLeGRqbE04TmEvSjFBQUJ3TVJRSFFIL2I2dTcKRlZSV2RHbzJETzg0Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K - server: https://127.0.0.1:51731 + certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCVENDQWUyZ0F3SUJBZ0lJYzFVQ2dTbkNVMTB3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TmpBeE1UTXhPVEkyTlRWYUZ3MHpOakF4TVRFeE9UTXhOVFZhTUJVeApFekFSQmdOVkJBTVRDbXQxWW1WeWJtVjBaWE13Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLCkFvSUJBUURQc05yb2wwckFta2VhR2x0UHlGN1ZDY211eTdUcWEvQnBLclRxbERnYnBVY2loRjZEYldRcHg2WHkKRFpsci9DTDVVRmw3U3BHbGtQR2EwSjUrMVoxWUJzQjhVczdQRFVIVGx4enNhVHdtT3RVL0xqSkdoTUNscFZqRgpFVFBwSVVnbzcrUzJOR014VWN4WFM4MldRcyt5NGZPdnU4WXVSSktVOGZITTZuUWErbU43R2Z3L3kwYnJGM2p0Ck9aY2dEL0FQcTBiR2R2bEJXdXNFd0R4ckJjRWRHWmF0MHUwWTZKNVRhUjVGR3k5RHQ1LzVnbDRVd1YxU3dISmMKczF5dFVsMHcxQ0VNQ3ZWbXFtTFJWY2dVV09nSlhadlZydElUZnVwYVBJVStFS2FOVHd3V29DdlloL05HbDcxLwpKZnlNYTE1UEFxWHYvRDdOR09UU3hWYzZQS3ZwQWdNQkFBR2pXVEJYTUE0R0ExVWREd0VCL3dRRUF3SUNwREFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUIwR0ExVWREZ1FXQkJScnIzK08vWStRTGU2UU0xQnVxL1RWbjhiKzVqQVYKQmdOVkhSRUVEakFNZ2dwcmRXSmxjbTVsZEdWek1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQzkvWlFXdXJWeApNcHdUaWFNQjJ4d2FQRmpJTkd2UG5DT29DUEZDaEtPTmpKUHM1b09Vd0tNdDJIZmlhRTc0K0pYNWRKSmFCa0pWCjMxblNOWlNLZWJ5YjZ6Ulo5Qlp5R0hYSVRTR2xVQW5UV1RaOWlxNjZkdzRWSUlPNEhSLzRFSnJSNHZQZmFhR0sKNmVEcnpQNnJUNmxzY1hyNGdGQnRoRHJibU5NRkxVVEdIOTU4cHhDQ3p0MTBodVdlNjM2NU5xVHR0SEJWMHdpawpYSDU2VGg3ZkVodmszSWo2S1l4ZmJ6VXRLeHppaXVLTG40b1JvZWVrdHFwMmpERUtaZS8yekRiQXB6SHhzV3JwCnpnOHJqSXFGb3owSE52aGZNNlhrTTFGZ0J4SGgvdkdIQWJCcm9NNkJqODEwcGxzbktKNmUwbVdJbGk4RHE1K3AKTUE1VjIwRVA5WHJVCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + server: https://127.0.0.1:64749 name: kind-dns-upstream contexts: - context: @@ -14,6 +14,6 @@ kind: Config users: - name: kind-dns-upstream user: - client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURLVENDQWhHZ0F3SUJBZ0lJYUdHaGJrZTFUV0V3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TmpBeE1USXlNRE0xTURGYUZ3MHlOekF4TVRJeU1EUXdNREZhTUR3eApIekFkQmdOVkJBb1RGbXQxWW1WaFpHMDZZMngxYzNSbGNpMWhaRzFwYm5NeEdUQVhCZ05WQkFNVEVHdDFZbVZ5CmJtVjBaWE10WVdSdGFXNHdnZ0VpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCRHdBd2dnRUtBb0lCQVFERnpRVlMKZ2tmWENlTWtMekNCV0pOUXdCbnptQmdBVHFwYjZqaU5zT1RTL2NSMy93WksrU3RjRTljRmFmOUVMQWE1NVgvMQo4MExhdkhiVVVEc3pNM3lMRE5SbmZPYUFmWCtBOENRSzFYTWEyYnRXcGZOVDFWUzJTSGxaNlRNVTZyMDBIanVkCm83YTBCdmRxcDE3TUMrT2UzSUlXZnRmUVcwRUtLWHowWENlcjNibHlpRjBBa28ydmNUWFRtdmhpb25ERGtlSEkKVnVFVUN4YTQxb0FHcXBxWXo4VGltY2dYQ2QzQ3Vhdm9sUFhMdUpGN3NJNWRCNzNKNDlqN2Y5LzdiRWVqZGNadQpUc0hVT2NXOHdsOHJDNHN2NVZidWRoUk9vQ0VoblJ0U3gzWWcxdkN3cGw2QUFhS3FpNThwM1g2VklqKzZmMlZYCmtQUlZ4Ukp3aUl4ZUkyeTlBZ01CQUFHalZqQlVNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUsKQmdnckJnRUZCUWNEQWpBTUJnTlZIUk1CQWY4RUFqQUFNQjhHQTFVZEl3UVlNQmFBRkZNL3hXanZzR2l2aUFKZgpzZCsvOENoNENzUndNQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUJEL3owMWF2QkZFeERFWDduTHNBNGlRb3VhCm1kSmF2NWNsOTlJdTVZM2R6bXZ4anRJNk81VTlaNzdVeVFFK1VaTHl1KzZtKzVnbVZSQ1R3eVRNdWxxS1RaVWEKbDB1ZFFJTG9YbGhzR3NUdmJ1UmtnZTRTcnhVZWJZMFE3V3hsYzE1dTJmVThUQzZ0MWlrcW5rM09CMGZzM0puWQprSmdzQzNhNnZNaVppZjZYdTMvZHkrZmtYb2JMV3ZiaGxoYmZteXpTVDArRVoxa294M2FYTDMrcDF0TGZ3NzN3CmJoeWo5a1VYcEl1ZnJnNXN4L0VTdDRGMitSanVMbS9RN2FibkYzYkVXRFB4TURVRnV2WkJDQkI0YWQ0bW9KTmQKdU4yYnp3R3oybWxRbVFpcy9US1Nzd2REN2s0WVRER215eVRQYlh2b0l6RkdtTjNlcnZ1b0tWZFdtYTFtCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K - client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb2dJQkFBS0NBUUVBeGMwRlVvSkgxd25qSkM4d2dWaVRVTUFaODVnWUFFNnFXK280amJEazB2M0VkLzhHClN2a3JYQlBYQlduL1JDd0d1ZVYvOWZOQzJyeDIxRkE3TXpOOGl3elVaM3ptZ0gxL2dQQWtDdFZ6R3RtN1ZxWHoKVTlWVXRraDVXZWt6Rk9xOU5CNDduYU8ydEFiM2FxZGV6QXZqbnR5Q0ZuN1gwRnRCQ2lsODlGd25xOTI1Y29oZApBSktOcjNFMTA1cjRZcUp3dzVIaHlGYmhGQXNXdU5hQUJxcWFtTS9FNHBuSUZ3bmR3cm1yNkpUMXk3aVJlN0NPClhRZTl5ZVBZKzMvZisyeEhvM1hHYms3QjFEbkZ2TUpmS3d1TEwrVlc3bllVVHFBaElaMGJVc2QySU5id3NLWmUKZ0FHaXFvdWZLZDErbFNJL3VuOWxWNUQwVmNVU2NJaU1YaU5zdlFJREFRQUJBb0lCQUJUM3h4TG9SeHZMUzBZTQpiNUNpbGxrMngvbDcyNzE2bVZJTmllbXhRUXlCeEtnd3cxYkN3NThxNWo0SGJyMG9DcDE5cjlzZmFxeWIwbC91CjBsdTYzejZ4UVRub01sb1lFNkpVTW9ub2R4OTNTY1hsYVI0dnJOOTIzdEJTYVcwVDlqTVdhbHpyWENTSTRZVHYKa1p2QlBlT2EvZnBLLzI4eG9Uc2x5ejV2SDNCM0xIU0JVZWZlV25oYlY5V0pESXRRVHgvZ1pSUXpIUmNJbG5NdgpDeWhZUmpqVksrOXVGZDNRRWZWcDk3RjNHNXUwbzdqQXJRQWcyWThrUjZ0dDVaZkR4clhkdCs0Z2RhVTBHZFE4Cm8rUTNlR1VUVEhUQm9nSHJwdDRwcHZXV0pITU11YW95VTZiK2p3OXN2RUdqVWphODRZYjNZUllGUmJYeUJZc0oKeGszSU4vRUNnWUVBek9ETGhWYVdIWjVpNUtTblhucy9pNDFmNjVOL2Rja1V0UlhQM0RURm1iRTdKWldkWFZNWApSYm4vRU1mcDV5MzFSK0pWYVpQVlpnR2x1TTllL21JSWY4ejZkVTk3ZmJNU05NTUZYZDdLQkNOZnVhVllxdUdrCk1SZkhBQ2V4M0Z0N29obnROUjhQbnh4VVljMEpXUkd6L2VJeU11b2lGRXRNMGFoQlkzYVIrZTBDZ1lFQTl5Z2wKUnE3MTIxOXB5YnNFOUVXcmtHZmxDblh5WDI1aTVMZXdkd1AzUGNzNFNYVDhKZTllRUVPZDFRTEZ3R2JVZ2djNQp0bFFtcFVhMG52WmtDYlp6T3dPQk5nWUtBVEpBcjk3TEpDVGNNcFJ4Sk1BSWJQMG8zVkVTL0U2OXliUmMzOWQxClVac293RkRTejFxd1I0WGdycTgxZ3FJZE45emNReENWS0NEdXBCRUNnWUJ2VWFFanBPVlIySkpSTzJtNU8yeE8KamhWVk1jSnFwRVE5RkVucGt6N2VnRjdyei93K0RmeXlKUnFDNnF5YnNPdjZEKzlxdXltVEVFZ1VQNUNVMVgxYQp1MnhHdTFZVStXeG1BS1QwMlMyWXpBT2lJa1lvS3d3RXBLKzYxTmFlTFpMaWhBWFAvRDJIcldQbjgva2xUU29vClEzUVZHQVJHVkplN3Z4a3dTdWVNRFFLQmdFM05EMTdldUhuajRSTWxrZnVxNnNTOFQ3Y3BSYkNRdVFTeVpoUXcKNVdWSVVXR2VON2xoVGtUa1pBeW5vTVJlR2tzTUp6aWo2TDVpTVgxUXBsRUFZK21Sd3R6VXJkV09raHBLa2J2QQo5cWZkWG5ocEVyM3NPeTdmMUpBajRVNWJQbGtnSThnYWhZdDBaY2ZzRGsyVmNSTE1DSllrbmZuMXhrZytNaFc5CnVDRmhBb0dBSEw3SGVZZTlxQzgvSDh4a0l2MnYrRE1WT1hSWVdwMStLZEhMbkUwWkZIdDhIdDlNWmZiRkZWM0wKMk1taDVWMUNLUS9CN3VNOWVhUW1SZVJDWEExcUZTQlhWaG8rd1daSWh0WnhmVFZVQWVLWkR1U0VjanM4MGlRVApMbmlMbWtrSFFhd01GVnJhSWFKczYvUEc0QjZheDFGRTZDSS85T2lDaXZzMXdwMlFKWjQ9Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg== + client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURLVENDQWhHZ0F3SUJBZ0lJVEl4Y21VZ1BnUFF3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TmpBeE1UTXhPVEkyTlRWYUZ3MHlOekF4TVRNeE9UTXhOVFZhTUR3eApIekFkQmdOVkJBb1RGbXQxWW1WaFpHMDZZMngxYzNSbGNpMWhaRzFwYm5NeEdUQVhCZ05WQkFNVEVHdDFZbVZ5CmJtVjBaWE10WVdSdGFXNHdnZ0VpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCRHdBd2dnRUtBb0lCQVFETy9uSHkKenN5MGFzOUlNQjFIZzk4YjJ0ZCtsSFhNVDY3aTFHTHNkcHVVMm55eWdKMFNpd25pQkpEbnVxWHJXMW45S2xxNQowSWdBVXhxbkZOTzhjQ1c3M3cvWGZhY2hiRjZEeTY5bGZHcUhMNCt2VDNiRnhqZW5aYzlOdE1tYWJSZUFZd2hDCjR3aHJvRUY4UC9Vb2trNFR1V01UZzlZRC9kem5kWlRaTTQyNzBVMnN2TVVKK3lhall0WEhCKzIwNWlTcTZ3MWwKTFNMVkxJUktRamc4Q3paUnhjVjlKK3ZWU1pNbG9xMXEzd2VCZ0lhZXRhbFN5NVpKTTJJeVAwZFVLd1diaFQ5cgozTmJ6MGV0UWdxeU4xUDc2dE9mMkNDN1ArOUtxRVlEOUgwcnpLSW5xU0JNSVA4Z2RkOUFqNnVKRWpTTjhaVXptCkZmbFdlWDRxYm5vNkh2R1RBZ01CQUFHalZqQlVNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUsKQmdnckJnRUZCUWNEQWpBTUJnTlZIUk1CQWY4RUFqQUFNQjhHQTFVZEl3UVlNQmFBRkd1dmY0NzlqNUF0N3BBegpVRzZyOU5XZnh2N21NQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUE5SGticjNhaXJua0lNWnRYZ0xjeUczV0ZWClhGTVdzMmtrVzVMRHBSenZMQW9lVms4Yzd3MXJBUjhhRkpoaklqaDBtZTB2ckNCL3p1UWRtNFVacjFqZkZtWlEKVWdqVG1WOWRqNm1JSWxQb2pRckFSWVNlTDY4VFlNdjMyWThRZUJhYm9lWjBQZmI5SURXTTdBVW96WmFHT25TMgpKc1RhZWkyOHdmUXkydzBZNS9rekc5bDJKUFBiRHZLRVV2czhIZTNrS1ZuNWtkMWNFcWlJU1llbnJRNS9IcGN4Cm90THl4cUtDbEdSZ3N3VmNRZDRudU1ML1JPaW1lK2RHQVQvNTlTZnpEYXZwb2tsdlN6d3IrZGNtdDV3dXB6QnYKZStKc244SjJhU1VROFBRaGFrMEdSTVlSWWlSdnN1L0JMSVdFN1FrZ0w5WkxjdnRHSkNRdW9DZnRWdUplCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBenY1eDhzN010R3JQU0RBZFI0UGZHOXJYZnBSMXpFK3U0dFJpN0hhYmxOcDhzb0NkCkVvc0o0Z1NRNTdxbDYxdFovU3BhdWRDSUFGTWFweFRUdkhBbHU5OFAxMzJuSVd4ZWc4dXZaWHhxaHkrUHIwOTIKeGNZM3AyWFBUYlRKbW0wWGdHTUlRdU1JYTZCQmZELzFLSkpPRTdsakU0UFdBLzNjNTNXVTJUT051OUZOckx6RgpDZnNtbzJMVnh3ZnR0T1lrcXVzTlpTMGkxU3lFU2tJNFBBczJVY1hGZlNmcjFVbVRKYUt0YXQ4SGdZQ0ducldwClVzdVdTVE5pTWo5SFZDc0ZtNFUvYTl6Vzg5SHJVSUtzamRUKytyVG45Z2d1ei92U3FoR0EvUjlLOHlpSjZrZ1QKQ0QvSUhYZlFJK3JpUkkwamZHVk01aFg1Vm5sK0ttNTZPaDd4a3dJREFRQUJBb0lCQUdKZWgzWjFrdERXeGFVdgp3R3BoSUNGVHNmOWt2RXFaUDZwcWRveWJuVHB6VHJsaDU4T05NZWdvZFZpNjJlanNvK3B0TzJwODBIVWZDVmFICnprd0tHOVNab0NTdmdVS2dCcGFwc0xRUkdXc2ZUakJwR2kvSkVGL01RV1ZUV2srNk1tWUFLa2ZuTHZRKzE0QWQKd1B0RDlEanBiRTAwNVB0R3BMbVdwbU5HWGIvNFVBT0lsZU9ubWpZWVVvUE50Q1JKa0pHQ2JOa3pkMnNkUDFweQpwQUVxU3F5dklONkZndnJMTnFjUFp2QnV6MnBVdFlLMFRsaUg5Yy9hbTFLdUJoSVpaSHdkVGNRZFlUczRJcU9KCnNqcHN1cURzNVcrazV6WDhKdGZ0ZUdIOFd5end3L2krdnlXc0V6cnFLZ2dPemhJWnhzSnhRNXJHeGwvSDhLVnUKcTFINUJwa0NnWUVBM3hXbzd5eXJmNGdnaENrRVRNVmMxYTdGcG1vK2ZVQkhXK0xWdFF5NEdWZHY0U0VpK1FabwpZeUd2SlYrN2ZVTjlsSnpqUVZWbVkraWhFVUI5VTZTUFQ1NmNhYXhnMFVlY1drSm0xUUJoL21sNTJWNS9mSnI3CkxiOStyYnBlcER1aVhPNXpiZC9RaVVpUURyWnVzNXAzSHMzd3hlSU5BYXhyeW1XdW4ybUVKd1VDZ1lFQTdZa0MKUUFzbDNZWFl2ODd6dFN3bWRlbVdGUUpQazE3U0x1QVdnY0daT2xTUVdvY04ycnI0V3VEazZLSXpkbkNrc1RtVAp1ekNzeTRqdlJUaUlyZjJtVTJvQ252SEJJMWxMZ1lYUkIyb1VuOGFqTURqZndtNzQ5ZFJlcVRheVRvSjkyN0lhClhXV3hpN0dDSlNWeEJOdzE0d3VFZXpaSllpTDZXNkorNWFydWFiY0NnWUFpbklHeFdnVGhySVVlL0I0bXF4aFUKTHVHTGlFQlp2bmRUMGtYRjZVdEc0MElBYzl1eE4wVkszQmNJZlduaGJXODJkNERxeWcwd3d3NzZWajhia3hTSgpEZHJHcW0vN0NGbEJ4N3Vjb0lxVHBsbTVWK2YvdFN2elZScWFhYWYxWXlzMXIrbEl5c2pZQStJVjVrZ1dwWWlGCnh2M3NOYjQrM0RsOUZYbWFVZ3ltNFFLQmdFUTUvak4zQUVGSW1LRS9TRERacFpKb3JYc0xWdC8xZEZtU2MrU0IKUHduS0VFeHdUa0p0UWJpWXNDZEJyNVp0ZEdDVE1TT3JMM2FtdGxNamtkNm41SVpCQk0reWtNOGVidG1kSGhVTApHekZwVktZZEwrZ2hCOUZVVm53MEFiTWJPQnRLWk5nK3hXaGliQWRQWWM4TGtVN05tQmZyMTlnZ1E5amVLNlM4CkhBNnhBb0dCQU5GL2hURzJVbGZRaUVYb3NkcUVUTE1jcFBYc1JORm1yV0U4Um00OVhmZzB4ajZRNWhCcFFVQnAKRGdCWXo5c213ODd0SHZzdDFtZ0drcE5qcHc4TjFRMlhiWkcxaGZtcGRuL1V4OU1NTzdQZXNoc0luUGFiL2FlawoxZ0haUjhaNFp0c2c1Ti9zTXRDOG5zQnpod0hOTnEybVRGcnoxR0ZpbUhENWxJc0V5MW5mCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg== diff --git a/test/e2e/tsig/chainsaw-test.yaml b/test/e2e/tsig/chainsaw-test.yaml index 025c44a..db55fdc 100644 --- a/test/e2e/tsig/chainsaw-test.yaml +++ b/test/e2e/tsig/chainsaw-test.yaml @@ -119,13 +119,13 @@ spec: name: example-com-tsig namespace: ($downstreamNamespaceName) - - name: Create generated TSIGKey upstream (no secretRef) + - name: Create generated DNSZoneTSIGKey upstream (no secretRef) try: - create: cluster: upstream resource: apiVersion: dns.networking.miloapis.com/v1alpha1 - kind: TSIGKey + kind: DNSZoneTSIGKey metadata: name: example-com-tsig-gen spec: @@ -137,7 +137,7 @@ spec: cluster: upstream resource: apiVersion: dns.networking.miloapis.com/v1alpha1 - kind: TSIGKey + kind: DNSZoneTSIGKey metadata: name: example-com-tsig-gen ownerReferences: @@ -145,7 +145,7 @@ spec: kind: DNSZone name: example-com-tsig - - name: Wait for generated TSIGKey Accepted/Programmed upstream + - name: Wait for generated DNSZoneTSIGKey Accepted/Programmed upstream try: - sleep: duration: 5s @@ -153,10 +153,11 @@ spec: cluster: upstream resource: apiVersion: dns.networking.miloapis.com/v1alpha1 - kind: TSIGKey + kind: DNSZoneTSIGKey metadata: name: example-com-tsig-gen status: + tsigKeyName: datum-example-com-tsig-gen.example-tsig.com. conditions: - type: Accepted status: "True" @@ -173,7 +174,7 @@ spec: metadata: name: example-com-tsig-gen - - name: Confirm generated TSIGKey + Secret exist downstream + - name: Confirm generated DNSZoneTSIGKey + Secret exist downstream try: - script: cluster: upstream @@ -188,7 +189,7 @@ spec: cluster: downstream resource: apiVersion: dns.networking.miloapis.com/v1alpha1 - kind: TSIGKey + kind: DNSZoneTSIGKey metadata: name: example-com-tsig-gen namespace: ($downstreamNamespaceName) @@ -230,7 +231,7 @@ spec: name: example-com-tsig-gen namespace: ($downstreamNamespaceName) - - name: Delete downstream generated TSIGKey and ensure it + Secret are recreated + - name: Delete downstream generated DNSZoneTSIGKey and ensure it + Secret are recreated try: - script: cluster: upstream @@ -246,17 +247,17 @@ spec: content: | set -euo pipefail # Find the mapped downstream namespace by label selector, then delete. - ns=$(kubectl get tsigkey -A \ + ns=$(kubectl get dnszonetsigkeys -A \ -l meta.datumapis.com/upstream-namespace="$NAMESPACE",meta.datumapis.com/upstream-name=example-com-tsig-gen \ -o jsonpath='{.items[0].metadata.namespace}') - kubectl -n "$ns" delete tsigkey example-com-tsig-gen --wait=false --ignore-not-found=true + kubectl -n "$ns" delete dnszonetsigkey example-com-tsig-gen --wait=false --ignore-not-found=true - sleep: duration: 8s - assert: cluster: downstream resource: apiVersion: dns.networking.miloapis.com/v1alpha1 - kind: TSIGKey + kind: DNSZoneTSIGKey metadata: name: example-com-tsig-gen namespace: ($downstreamNamespaceName) @@ -269,7 +270,7 @@ spec: name: example-com-tsig-gen namespace: ($downstreamNamespaceName) - - name: Delete upstream generated TSIGKey and ensure downstream TSIGKey + Secret are GC'd + - name: Delete upstream generated DNSZoneTSIGKey and ensure downstream DNSZoneTSIGKey + Secret are GC'd try: - script: cluster: upstream @@ -284,7 +285,7 @@ spec: cluster: upstream ref: apiVersion: dns.networking.miloapis.com/v1alpha1 - kind: TSIGKey + kind: DNSZoneTSIGKey name: example-com-tsig-gen - sleep: duration: 10s @@ -292,7 +293,7 @@ spec: cluster: downstream resource: apiVersion: dns.networking.miloapis.com/v1alpha1 - kind: TSIGKey + kind: DNSZoneTSIGKey metadata: name: example-com-tsig-gen namespace: ($downstreamNamespaceName) @@ -305,7 +306,7 @@ spec: name: example-com-tsig-gen namespace: ($downstreamNamespaceName) - - name: Create BYO Secret + TSIGKey upstream + - name: Create BYO Secret + DNSZoneTSIGKey upstream try: - create: cluster: upstream @@ -321,7 +322,7 @@ spec: cluster: upstream resource: apiVersion: dns.networking.miloapis.com/v1alpha1 - kind: TSIGKey + kind: DNSZoneTSIGKey metadata: name: example-com-tsig-byo spec: @@ -332,7 +333,7 @@ spec: secretRef: name: byo-tsig - - name: Wait for BYO TSIGKey Accepted/Programmed upstream + - name: Wait for BYO DNSZoneTSIGKey Accepted/Programmed upstream try: - sleep: duration: 5s @@ -340,10 +341,11 @@ spec: cluster: upstream resource: apiVersion: dns.networking.miloapis.com/v1alpha1 - kind: TSIGKey + kind: DNSZoneTSIGKey metadata: name: example-com-tsig-byo status: + tsigKeyName: datum-example-com-tsig-byo.example-tsig.com. conditions: - type: Accepted status: "True" @@ -413,7 +415,7 @@ spec: cluster: upstream resource: apiVersion: dns.networking.miloapis.com/v1alpha1 - kind: TSIGKey + kind: DNSZoneTSIGKey metadata: name: example-com-tsig-bad spec: @@ -429,7 +431,7 @@ spec: cluster: upstream resource: apiVersion: dns.networking.miloapis.com/v1alpha1 - kind: TSIGKey + kind: DNSZoneTSIGKey metadata: name: example-com-tsig-bad status: @@ -444,19 +446,19 @@ spec: cluster: upstream ref: apiVersion: dns.networking.miloapis.com/v1alpha1 - kind: TSIGKey + kind: DNSZoneTSIGKey name: example-com-tsig-gen - delete: cluster: upstream ref: apiVersion: dns.networking.miloapis.com/v1alpha1 - kind: TSIGKey + kind: DNSZoneTSIGKey name: example-com-tsig-byo - delete: cluster: upstream ref: apiVersion: dns.networking.miloapis.com/v1alpha1 - kind: TSIGKey + kind: DNSZoneTSIGKey name: example-com-tsig-bad - delete: cluster: upstream From 0fec378ac3fc5e58a34dfca30476bb4b84632dfa Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Tue, 13 Jan 2026 12:06:21 -0800 Subject: [PATCH 3/4] fix: lint --- .../dnszonetsigkey_powerdns_controller.go | 4 +- .../dnszonetsigkey_replicator_controller.go | 14 +++--- ...szonetsigkey_replicator_controller_test.go | 1 - internal/pdns/client.go | 19 ++++---- internal/pdns/pdns_integration_test.go | 43 ++++++++----------- internal/pdns/pdns_test.go | 10 +++-- test/e2e/kubeconfig-downstream | 19 -------- test/e2e/kubeconfig-upstream | 19 -------- 8 files changed, 43 insertions(+), 86 deletions(-) delete mode 100644 test/e2e/kubeconfig-downstream delete mode 100644 test/e2e/kubeconfig-upstream diff --git a/internal/controller/dnszonetsigkey_powerdns_controller.go b/internal/controller/dnszonetsigkey_powerdns_controller.go index bbed702..37bb850 100644 --- a/internal/controller/dnszonetsigkey_powerdns_controller.go +++ b/internal/controller/dnszonetsigkey_powerdns_controller.go @@ -165,7 +165,7 @@ func (r *DNSZoneTSIGKeyPowerDNSReconciler) Reconcile(ctx context.Context, req ct } // Resolve key material from Secret (BYO) or generated secret. - secretName, keyMaterial, ok, err := r.resolveKeyMaterial(ctx, &tk, string(alg)) + secretName, keyMaterial, ok, err := r.resolveKeyMaterial(ctx, &tk) if err != nil { return ctrl.Result{}, err } @@ -269,7 +269,7 @@ func (r *DNSZoneTSIGKeyPowerDNSReconciler) resolveZoneClass(ctx context.Context, return &zc, true, nil } -func (r *DNSZoneTSIGKeyPowerDNSReconciler) resolveKeyMaterial(ctx context.Context, tk *dnsv1alpha1.DNSZoneTSIGKey, algorithm string) (secretName string, keyMaterial string, ok bool, err error) { +func (r *DNSZoneTSIGKeyPowerDNSReconciler) resolveKeyMaterial(ctx context.Context, tk *dnsv1alpha1.DNSZoneTSIGKey) (secretName string, keyMaterial string, ok bool, err error) { // BYO secret: validate schema and do not mutate. if tk.Spec.SecretRef != nil && tk.Spec.SecretRef.Name != "" { var s corev1.Secret diff --git a/internal/controller/dnszonetsigkey_replicator_controller.go b/internal/controller/dnszonetsigkey_replicator_controller.go index 88fd85d..ce44af8 100644 --- a/internal/controller/dnszonetsigkey_replicator_controller.go +++ b/internal/controller/dnszonetsigkey_replicator_controller.go @@ -121,12 +121,12 @@ func (r *DNSZoneTSIGKeyReplicator) Reconcile(ctx context.Context, req mcreconcil } // Ensure downstream shadow DNSZoneTSIGKey mirrors upstream spec. - if _, err := r.ensureDownstreamDNSZoneTSIGKey(ctx, req.ClusterName, strategy, &upstream); err != nil { + if err := r.ensureDownstreamDNSZoneTSIGKey(ctx, req.ClusterName, strategy, &upstream); err != nil { return ctrl.Result{}, err } // Ensure Secret is present upstream and replicated to downstream so PowerDNS can consume it. - if err := r.ensureSecretReplication(ctx, req.ClusterName, upstreamCluster.GetClient(), strategy, &upstream); err != nil { + if err := r.ensureSecretReplication(ctx, upstreamCluster.GetClient(), strategy, &upstream); err != nil { return ctrl.Result{}, err } @@ -199,10 +199,10 @@ func (r *DNSZoneTSIGKeyReplicator) handleDeletion(ctx context.Context, c client. return true, nil } -func (r *DNSZoneTSIGKeyReplicator) ensureDownstreamDNSZoneTSIGKey(ctx context.Context, upstreamClusterName string, strategy downstreamclient.ResourceStrategy, upstream *dnsv1alpha1.DNSZoneTSIGKey) (controllerutil.OperationResult, error) { +func (r *DNSZoneTSIGKeyReplicator) ensureDownstreamDNSZoneTSIGKey(ctx context.Context, upstreamClusterName string, strategy downstreamclient.ResourceStrategy, upstream *dnsv1alpha1.DNSZoneTSIGKey) error { md, err := strategy.ObjectMetaFromUpstreamObject(ctx, upstream) if err != nil { - return controllerutil.OperationResultNone, err + return err } shadow := dnsv1alpha1.DNSZoneTSIGKey{} @@ -224,10 +224,10 @@ func (r *DNSZoneTSIGKeyReplicator) ensureDownstreamDNSZoneTSIGKey(ctx context.Co return strategy.SetControllerReference(ctx, upstream, &shadow) }) if cErr != nil { - return res, cErr + return cErr } log.FromContext(ctx).Info("ensured downstream DNSZoneTSIGKey", "operation", res, "namespace", shadow.Namespace, "name", shadow.Name) - return res, nil + return nil } func (r *DNSZoneTSIGKeyReplicator) updateStatus(ctx context.Context, c client.Client, upstream *dnsv1alpha1.DNSZoneTSIGKey, downstreamStatus *dnsv1alpha1.DNSZoneTSIGKeyStatus) error { @@ -242,7 +242,7 @@ func (r *DNSZoneTSIGKeyReplicator) updateStatus(ctx context.Context, c client.Cl return c.Status().Patch(ctx, upstream, client.MergeFrom(base)) } -func (r *DNSZoneTSIGKeyReplicator) ensureSecretReplication(ctx context.Context, upstreamClusterName string, upstreamClient client.Client, strategy downstreamclient.ResourceStrategy, upstream *dnsv1alpha1.DNSZoneTSIGKey) error { +func (r *DNSZoneTSIGKeyReplicator) ensureSecretReplication(ctx context.Context, upstreamClient client.Client, strategy downstreamclient.ResourceStrategy, upstream *dnsv1alpha1.DNSZoneTSIGKey) error { // Determine the source secret name. secretName := upstream.Name if upstream.Spec.SecretRef != nil && upstream.Spec.SecretRef.Name != "" { diff --git a/internal/controller/dnszonetsigkey_replicator_controller_test.go b/internal/controller/dnszonetsigkey_replicator_controller_test.go index 6e7343c..014648d 100644 --- a/internal/controller/dnszonetsigkey_replicator_controller_test.go +++ b/internal/controller/dnszonetsigkey_replicator_controller_test.go @@ -23,7 +23,6 @@ func TestTsigKeySecretLen_EqualsHashOutputSize(t *testing.T) { } for _, tc := range cases { - tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() if got := tsigKeySecretLen(tc.alg); got != tc.want { diff --git a/internal/pdns/client.go b/internal/pdns/client.go index b5a9cde..aaf0067 100644 --- a/internal/pdns/client.go +++ b/internal/pdns/client.go @@ -67,16 +67,15 @@ func (e *pdnsAPIError) Error() string { return fmt.Sprintf("error: status %d", e.Status) } -func readRespBody(resp *http.Response, max int64) string { +const maxErrorBodyBytes int64 = 64 << 10 // 64 KiB + +func readRespBody(resp *http.Response) string { if resp == nil || resp.Body == nil { return "" } defer func() { _ = resp.Body.Close() }() - // don't blow up logs; cap at e.g. 16KB - if max <= 0 { - max = 16 << 10 // 16 KiB - } - b, _ := io.ReadAll(io.LimitReader(resp.Body, max)) + // don't blow up logs; cap response bodies + b, _ := io.ReadAll(io.LimitReader(resp.Body, maxErrorBodyBytes)) return strings.TrimSpace(string(b)) } @@ -160,7 +159,7 @@ func (c *Client) ListTSIGKeys(ctx context.Context) ([]TSIGKey, error) { return nil, err } if resp.StatusCode/100 != 2 { - errBody := readRespBody(resp, 64<<10) // closes Body + errBody := readRespBody(resp) // closes Body return nil, &pdnsAPIError{Status: resp.StatusCode, Body: errBody} } defer func() { _ = resp.Body.Close() }() @@ -192,7 +191,7 @@ func (c *Client) CreateTSIGKey(ctx context.Context, name, algorithm, keyMaterial return TSIGKey{}, err } if resp.StatusCode/100 != 2 { - errBody := readRespBody(resp, 64<<10) // closes Body + errBody := readRespBody(resp) // closes Body return TSIGKey{}, &pdnsAPIError{Status: resp.StatusCode, Body: errBody} } defer func() { _ = resp.Body.Close() }() @@ -258,7 +257,7 @@ func (c *Client) DeleteTSIGKey(ctx context.Context, id string) error { return nil } if resp.StatusCode/100 != 2 { - errBody := readRespBody(resp, 64<<10) // closes Body + errBody := readRespBody(resp) // closes Body return &pdnsAPIError{Status: resp.StatusCode, Body: errBody} } _ = resp.Body.Close() @@ -536,7 +535,7 @@ func (c *Client) applyRRSetPatch(ctx context.Context, zone string, patch []rrset return err } if resp.StatusCode/100 != 2 { - errBody := readRespBody(resp, 64<<10) // closes Body + errBody := readRespBody(resp) // closes Body return &pdnsAPIError{Status: resp.StatusCode, Body: errBody} } _ = resp.Body.Close() diff --git a/internal/pdns/pdns_integration_test.go b/internal/pdns/pdns_integration_test.go index 6493e01..c193f8d 100644 --- a/internal/pdns/pdns_integration_test.go +++ b/internal/pdns/pdns_integration_test.go @@ -45,13 +45,15 @@ func writePDNSAuthWithSQLite(t *testing.T, dir, apiKey string) { } } -func startPDNS(t *testing.T, apiKey string) (baseURL string, terminate func()) { +const integrationAPIKey = "itest-key" + +func startPDNS(t *testing.T) (baseURL string, terminate func()) { t.Helper() ctx := context.Background() cfgDir := t.TempDir() dataDir := t.TempDir() - writePDNSAuthWithSQLite(t, cfgDir, apiKey) + writePDNSAuthWithSQLite(t, cfgDir, integrationAPIKey) // Use an official-ish PDNS authoritative image that reads /etc/powerdns/pdns.conf. // You can pin a specific version if you prefer, e.g. powerdns/pdns-auth-46:latest @@ -76,7 +78,7 @@ func startPDNS(t *testing.T, apiKey string) (baseURL string, terminate func()) { }, WaitingFor: wait.ForHTTP("/api/v1/servers/localhost"). WithPort("8081/tcp"). - WithHeaders(map[string]string{"X-API-Key": apiKey}). + WithHeaders(map[string]string{"X-API-Key": integrationAPIKey}). WithStartupTimeout(2 * time.Minute), }, Started: true, @@ -104,11 +106,10 @@ func startPDNS(t *testing.T, apiKey string) (baseURL string, terminate func()) { func TestPDNS_EndToEnd_AllTypes(t *testing.T) { // No t.Parallel(): we’re booting a container. - const apiKey = "itest-key" - baseURL, stop := startPDNS(t, apiKey) + baseURL, stop := startPDNS(t) defer stop() - client := NewClient(baseURL, apiKey) + client := NewClient(baseURL, integrationAPIKey) zone := "example.test" // Create the zone with some NS via the API create call @@ -328,11 +329,10 @@ func TestPDNS_EndToEnd_AllTypes(t *testing.T) { func TestPDNS_ApplyRecordSetAuthoritative_CleansRemovedOwners(t *testing.T) { // No t.Parallel(): container + real PDNS. - const apiKey = "itest-key" - baseURL, stop := startPDNS(t, apiKey) + baseURL, stop := startPDNS(t) defer stop() - client := NewClient(baseURL, apiKey) + client := NewClient(baseURL, integrationAPIKey) ctx := context.Background() zone := "cleanup.test" @@ -436,11 +436,10 @@ func TestPDNS_ApplyRecordSetAuthoritative_CleansRemovedOwners(t *testing.T) { func TestPDNS_TSIGKey_CreateWithSuppliedKey(t *testing.T) { // No t.Parallel(): container + real PDNS. - const apiKey = "itest-key" - baseURL, stop := startPDNS(t, apiKey) + baseURL, stop := startPDNS(t) defer stop() - client := NewClient(baseURL, apiKey) + client := NewClient(baseURL, integrationAPIKey) ctx := context.Background() keyMaterial := base64.StdEncoding.EncodeToString([]byte("supersecret")) @@ -461,11 +460,10 @@ func TestPDNS_TSIGKey_CreateWithSuppliedKey(t *testing.T) { func TestPDNS_TSIGKey_CreateAndDeleteByID(t *testing.T) { // No t.Parallel(): container + real PDNS. - const apiKey = "itest-key" - baseURL, stop := startPDNS(t, apiKey) + baseURL, stop := startPDNS(t) defer stop() - client := NewClient(baseURL, apiKey) + client := NewClient(baseURL, integrationAPIKey) ctx := context.Background() keyMaterial := base64.StdEncoding.EncodeToString([]byte("supersecret")) @@ -492,11 +490,10 @@ func TestPDNS_TSIGKey_CreateAndDeleteByID(t *testing.T) { func TestPDNS_TSIGKey_IDHasTrailingDot_AndDuplicateNameIsRejected(t *testing.T) { // No t.Parallel(): container + real PDNS. - const apiKey = "itest-key" - baseURL, stop := startPDNS(t, apiKey) + baseURL, stop := startPDNS(t) defer stop() - client := NewClient(baseURL, apiKey) + client := NewClient(baseURL, integrationAPIKey) ctx := context.Background() // Use the same provider-visible name twice. @@ -531,11 +528,10 @@ func TestPDNS_TSIGKey_IDHasTrailingDot_AndDuplicateNameIsRejected(t *testing.T) func TestPDNS_TSIGKey_NameWithTrailingDot_IDHasSingleTrailingDot(t *testing.T) { // No t.Parallel(): container + real PDNS. - const apiKey = "itest-key" - baseURL, stop := startPDNS(t, apiKey) + baseURL, stop := startPDNS(t) defer stop() - client := NewClient(baseURL, apiKey) + client := NewClient(baseURL, integrationAPIKey) ctx := context.Background() const nameWithDot = "mytsigkey-trailing-dot." @@ -567,11 +563,10 @@ func TestPDNS_TSIGKey_NameWithTrailingDot_IDHasSingleTrailingDot(t *testing.T) { func TestPDNS_TSIGKey_DuplicateNameEvenWithIDFieldIsRejected(t *testing.T) { // No t.Parallel(): container + real PDNS. - const apiKey = "itest-key" - baseURL, stop := startPDNS(t, apiKey) + baseURL, stop := startPDNS(t) defer stop() - client := NewClient(baseURL, apiKey) + client := NewClient(baseURL, integrationAPIKey) ctx := context.Background() const name = "mytsigkey-dup-with-id-field" diff --git a/internal/pdns/pdns_test.go b/internal/pdns/pdns_test.go index 7da9558..b41eb45 100644 --- a/internal/pdns/pdns_test.go +++ b/internal/pdns/pdns_test.go @@ -593,6 +593,8 @@ func TestTSIGKey_CRUD(t *testing.T) { c := NewClient(srv.URL, "sekret") ctx := context.Background() + const hmacSHA256 = "hmac-sha256" + // list keys, err := c.ListTSIGKeys(ctx) if err != nil || len(keys) != 1 || keys[0].Name != "existing" { @@ -606,8 +608,8 @@ func TestTSIGKey_CRUD(t *testing.T) { } // create with key - created, err := c.CreateTSIGKey(ctx, "mykey", "hmac-sha256", "b64secret") - if err != nil || created.ID == "" || created.Name != "mykey" || created.Algorithm != "hmac-sha256" { + created, err := c.CreateTSIGKey(ctx, "mykey", hmacSHA256, "b64secret") + if err != nil || created.ID == "" || created.Name != "mykey" || created.Algorithm != hmacSHA256 { t.Fatalf("CreateTSIGKey got=%#v err=%v", created, err) } @@ -618,8 +620,8 @@ func TestTSIGKey_CRUD(t *testing.T) { } // ensure recreates when algorithm differs (existing removed, new created) - ens2, err := c.EnsureTSIGKey(ctx, "existing", "hmac-sha256", "b64secret2") - if err != nil || ens2.ID != "newid" || ens2.Name != "existing" || ens2.Algorithm != "hmac-sha256" { + ens2, err := c.EnsureTSIGKey(ctx, "existing", hmacSHA256, "b64secret2") + if err != nil || ens2.ID != "newid" || ens2.Name != "existing" || ens2.Algorithm != hmacSHA256 { t.Fatalf("EnsureTSIGKey recreate got=%#v err=%v", ens2, err) } diff --git a/test/e2e/kubeconfig-downstream b/test/e2e/kubeconfig-downstream deleted file mode 100644 index 99dd881..0000000 --- a/test/e2e/kubeconfig-downstream +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: v1 -clusters: -- cluster: - certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCVENDQWUyZ0F3SUJBZ0lJTFJpYlNEOWVQV013RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TmpBeE1UTXhPVEkyTVRKYUZ3MHpOakF4TVRFeE9UTXhNVEphTUJVeApFekFSQmdOVkJBTVRDbXQxWW1WeWJtVjBaWE13Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLCkFvSUJBUUN2aDk3MnFPcDJTUHRCOWU3ZlRPMHVrUkVzaXkrcmtWNytqS1hxTkFmNWlTQkx1TmVVdjdOSDFSMjMKaUNEaVU2UFZxY3NRZzU1YmpmWThndmZ0OXZJVGxFZG1tbFl3NUtlYkh5cGRSY3hXb1FvY1hEbTV3aXdkdGpKTQpncEpId0VreDhOYXlJalliVk0zcDhoSjArcW1EQUREbHkxSVRpNjZmSU1Ga3R1RFM4bnhxWWRRdVhUdzZIbHhPCldoMlM4em1IZ3c4SkFTSTFNdm51Wnk1c3UvWnQ4ck9odWUwWDVXQ2VoSDI3NGtMSCtFa3V4dm16MVV3NzAxSmgKdzFRTmVSTWVmdExPQUl6K0lXemV2WHB1WXZtS2x2a0ZTV01zb3I3NTM3eHdCclh1eVdlWnFiZzdTUlFyS3gvYwpyMk1VS29UOGw0RHRBWkFRWHZObkZSamlQL1hMQWdNQkFBR2pXVEJYTUE0R0ExVWREd0VCL3dRRUF3SUNwREFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUIwR0ExVWREZ1FXQkJSSGdSWTFsRjlia2NTK3ZsS3QzZUxIcnVHSjlqQVYKQmdOVkhSRUVEakFNZ2dwcmRXSmxjbTVsZEdWek1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQjRiY3BKOEJ1RgpjTTErRVNldTNYamdSb2taSG5BeTF5WXJudXlxdUxxbXpXUWFxemdpUXp5VlhKOC9IaTZUM3RSQXFQUGN0OG1JCkdDeTZrc3VPejlOZk5kdERzaGNDWlZ6ZTh5NjBHT2ZFTkhkL01QbWFBM0YvMUpCZTIwMEtpeExENkZmODdicXoKVlZzNjl2ZjMxdG5zNU13MEZ1RVVHdncvOFRSdjVqblBXVktTTjJvMWxJUy9Delh0MzEzclZYakc2Zk9TOGsxWApLMXVkRkoxQ0FiM1dJQlNMbjRMZlJ5KzdyU0EzUExCM05iMUlXNGNCRFRZbEQ3K1ZhTkdRalR5V29pb3BnNlp6CnR1WjJ3SUhQcjk1VUJJYU53cWNxRExFakhNWGVCSnZxVkhxWE9OaXlXTHRBdHhqbjA2RVpsQW9nOWwzNUtoNXIKMUxvdEs3a0JPZFh3Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K - server: https://127.0.0.1:64712 - name: kind-dns-downstream -contexts: -- context: - cluster: kind-dns-downstream - user: kind-dns-downstream - name: kind-dns-downstream -current-context: kind-dns-downstream -kind: Config -users: -- name: kind-dns-downstream - user: - client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURLVENDQWhHZ0F3SUJBZ0lJSjFMN25nVngwVm93RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TmpBeE1UTXhPVEkyTVRKYUZ3MHlOekF4TVRNeE9UTXhNVEphTUR3eApIekFkQmdOVkJBb1RGbXQxWW1WaFpHMDZZMngxYzNSbGNpMWhaRzFwYm5NeEdUQVhCZ05WQkFNVEVHdDFZbVZ5CmJtVjBaWE10WVdSdGFXNHdnZ0VpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCRHdBd2dnRUtBb0lCQVFEcDFBSnQKeE9mckIyclJhemJqV1JvQ0NqYTVKNUZUd3JZelQ3WDFVMXU0MlYwWm4xZE44STlvRUkyZ1M5YXVRSzlIQzRXSQpXSktOSTZuY1p0SDRFU2ptdkc1UzBscXYrTDhBeW1CRS9vUmdBTmhSZFgzbkl4NFZJWXorQkxZY2pncXJPd2JaCkhVREZPQ20rbTZ5ZFpEQlpEdzZ3Y3ZxSDFiWjBTWkdBeFlSLyt6dXVtY2gwZFZ6N05pajIrU3Q2d0ZPSHptZDkKV1N1bkRHVUllRWxKOXNEaEdCcjl3dzN2V0R0Y3FQL2dUZVNhNWVXZlVtTUo0OUFkczNxUU1mTmVScFJPeS8zbApVT0lPbHZCMTVobXU5UG05NFNXQ3lFUzNXVVk0MDd6VWZaWk5DdzM1MUh4Y3pWR3RlUFY5YjVWUFhrL0ZKT25PCkl0OXp1MVNKSStUMk9LNnRBZ01CQUFHalZqQlVNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUsKQmdnckJnRUZCUWNEQWpBTUJnTlZIUk1CQWY4RUFqQUFNQjhHQTFVZEl3UVlNQmFBRkVlQkZqV1VYMXVSeEw2KwpVcTNkNHNldTRZbjJNQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUFheGtKTkl0bkpKYzNMY2FUQkoyV09OdXRxCnFoM0FSZUVTQkVlSGdRazMyYWdOS3cyYnJRcmpBd1lGV3p1T2xkMGFMbEo4RitEMk9WMTJ1YlZKd3A4bmFkR1AKa3ZaTVBLMXRUTlgwZ0U1cllqNFdsZG41UnZabmpJV1FDdW1IVjFDTURzeENKQVArOFJYRlVaSWp5TFJ1cGRjMgpLNEhrZldNanh3V3V6ci9qRDljeFZNaytXaXVrTEJDQjNiODNnVEllTHVGeSszeG0rMHpSejQxV0dlUkhsb29pCjBZUzRPdjdGTklaMmtqTHhWNG9WYkF6NEM3WDdDUE4yODhNVUh0Y3V6Qi9sMFpLa2gwdGtPY3VxQWYvZktyVTkKNnNTYk0zZDJRV1JHMnRMNFBtWnE4UlBGQndyRGF3UWJvSFl5eCtpUGVWS2xidzRmNG5yTkpHZGk5OXA3Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K - client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBNmRRQ2JjVG42d2RxMFdzMjQxa2FBZ28ydVNlUlU4SzJNMCsxOVZOYnVObGRHWjlYClRmQ1BhQkNOb0V2V3JrQ3ZSd3VGaUZpU2pTT3AzR2JSK0JFbzVyeHVVdEphci9pL0FNcGdSUDZFWUFEWVVYVjkKNXlNZUZTR00vZ1MySEk0S3F6c0cyUjFBeFRncHZwdXNuV1F3V1E4T3NITDZoOVcyZEVtUmdNV0VmL3M3cnBuSQpkSFZjK3pZbzl2a3Jlc0JUaDg1bmZWa3Jwd3hsQ0hoSlNmYkE0UmdhL2NNTjcxZzdYS2ovNEUza211WGxuMUpqCkNlUFFIYk42a0RIelhrYVVUc3Y5NVZEaURwYndkZVlacnZUNXZlRWxnc2hFdDFsR09OTzgxSDJXVFFzTitkUjgKWE0xUnJYajFmVytWVDE1UHhTVHB6aUxmYzd0VWlTUGs5aml1clFJREFRQUJBb0lCQUFSSVlBb1NhMjV0aUR1dAplSkV2eXhJSmpaNVU1dGZ1Zk80dVFDbG1ma2V5c0t1Y0R1N1V3SW5IL3lFREw4TVNkdENSUDNUQXJXQmQwZ2dxCmxNVEMrZERzVUFTKzZSdjFhWTdpUkNhS3BpYjdobVF0TlBhcWRTd2dCcmcrN2hTZHNTbzNJRkdTYU9MUitDc3QKYzdPUW8va2RuT1g0R09xY08xaFZOTnNLSFhNTEUwazhLTXNzcmtPRm13SitSRlMyckxBQlplbHVvQVpNUWFWUgpVQ0E5eXpCVk1lMFlualZ1Vzl0dU01a0pJMnNuNEEyRnFJZFJ4SUxVY1I4MG1sWk9nbEpURXZpalVXMlRrY3dXCjd0R0N0V1IvMVowNXhRV000NHUzY21Kb3pONXh2VDErSzFLazFNbjB0amZnZy9iRkhuaHpuQWt4Ynd3cG9tZmQKWCt5Ym1Ta0NnWUVBN1hjQ0UycDRzOGQ1RGFIaXFxRUVjNW5nY2tabjBGZ2lSUENrSmx1SnlQdGtWQkEzT2dMYwpsRWlscEtoekc2emxHTnlENFN5L1E4YUJvdHFDYkZVRExDMmZnT2FhQVhSVlhiNWdIazc4SENleUlWYU9UZkU4Cm1ZdGE5dEUvUVl5QTFqNTBjZmhOOC9TWG5YMWVTQngvSjY2cHJqWVU4RnptMWttSXBzaTdmWnNDZ1lFQS9CUlYKUXRGZWU5aGNmcVV1VnQyUEhZSGxYcEdob2xHVzZXTEx1V3Q0TkgxRXNoYzdMeVNYRS9kbHNYRWE1UjU2ZFNYQQpKUVZHV3p4NGU2MVd4Mnp4azl4SFBBdm1pUmltbnc4VnFIc3J3ZTRYTGxtU092YjdqQUdEdDNESm5Oa2Y0Q0RxCjVOdnlSdDlUUkxTV3hJR3pqM3BHWTJSbnpydW4zSEg4TUNGeWJWY0NnWUVBbXFZcnN1dGZTbTM1TjFpYm50WVkKYVJUb3FHT1R6b3JuWHBCOXh3Rk1mWmpESVVBaVIyUi90UTZPMmVwZWRNS252UVkzMlJqa1EwWnZQTmtqb1Z2SQpJaWhnUFhseENNdHpvUWFQNEkwK0FUUVUvVU02a0NZd2VpclloZStHUzdFdVl0anZ5eDJUM3ZJSEg2ajdFdW1FCklock5KTWpSNEN3UXBiUGtEQUtrb0VzQ2dZQlNkbGhaN21IcFE2TW1idVRVMTgvY2lFUy9oZ2FKTWdXYlBZMkYKajZtWUNpNnh6N1cxdTFPTTNZNnYyRjlDK3BCMnlDMnVMcWFRYkJ6QjRMZVZyNGJycHRES3pOM1NsWFRVYmJ2WgpETW9JdTlscmVUUEVCRTNQeENNUm5Gem42WU5xNzNuSCtrZXNkWndveXFiVGk5Wndwa0JtZlU4VUt3RkR0U29aCm1LZDFLd0tCZ1FDeDhIYjhPZHhpbGpHSXpPdEE5THFRRE5rY0xoVUpZb0hzNDIvUkl2TFV1ZGxibmpnSEZRYmcKQXJVOExHOGhxelQzSVNvRCszMFBQVHBDMk41am80WFFVRFBra2c0VnlwbURHMzlPZFdoUFUyRk0ya1ZpVWdScwp5MW95NTJsWk9LMmU1U1ZQb21YTUdOakdkNWRpUSt4SnBRTkpicVVMZGhsZWhIRVNxamtNemc9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo= - diff --git a/test/e2e/kubeconfig-upstream b/test/e2e/kubeconfig-upstream deleted file mode 100644 index b0a8be5..0000000 --- a/test/e2e/kubeconfig-upstream +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: v1 -clusters: -- cluster: - certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCVENDQWUyZ0F3SUJBZ0lJYzFVQ2dTbkNVMTB3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TmpBeE1UTXhPVEkyTlRWYUZ3MHpOakF4TVRFeE9UTXhOVFZhTUJVeApFekFSQmdOVkJBTVRDbXQxWW1WeWJtVjBaWE13Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLCkFvSUJBUURQc05yb2wwckFta2VhR2x0UHlGN1ZDY211eTdUcWEvQnBLclRxbERnYnBVY2loRjZEYldRcHg2WHkKRFpsci9DTDVVRmw3U3BHbGtQR2EwSjUrMVoxWUJzQjhVczdQRFVIVGx4enNhVHdtT3RVL0xqSkdoTUNscFZqRgpFVFBwSVVnbzcrUzJOR014VWN4WFM4MldRcyt5NGZPdnU4WXVSSktVOGZITTZuUWErbU43R2Z3L3kwYnJGM2p0Ck9aY2dEL0FQcTBiR2R2bEJXdXNFd0R4ckJjRWRHWmF0MHUwWTZKNVRhUjVGR3k5RHQ1LzVnbDRVd1YxU3dISmMKczF5dFVsMHcxQ0VNQ3ZWbXFtTFJWY2dVV09nSlhadlZydElUZnVwYVBJVStFS2FOVHd3V29DdlloL05HbDcxLwpKZnlNYTE1UEFxWHYvRDdOR09UU3hWYzZQS3ZwQWdNQkFBR2pXVEJYTUE0R0ExVWREd0VCL3dRRUF3SUNwREFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUIwR0ExVWREZ1FXQkJScnIzK08vWStRTGU2UU0xQnVxL1RWbjhiKzVqQVYKQmdOVkhSRUVEakFNZ2dwcmRXSmxjbTVsZEdWek1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQzkvWlFXdXJWeApNcHdUaWFNQjJ4d2FQRmpJTkd2UG5DT29DUEZDaEtPTmpKUHM1b09Vd0tNdDJIZmlhRTc0K0pYNWRKSmFCa0pWCjMxblNOWlNLZWJ5YjZ6Ulo5Qlp5R0hYSVRTR2xVQW5UV1RaOWlxNjZkdzRWSUlPNEhSLzRFSnJSNHZQZmFhR0sKNmVEcnpQNnJUNmxzY1hyNGdGQnRoRHJibU5NRkxVVEdIOTU4cHhDQ3p0MTBodVdlNjM2NU5xVHR0SEJWMHdpawpYSDU2VGg3ZkVodmszSWo2S1l4ZmJ6VXRLeHppaXVLTG40b1JvZWVrdHFwMmpERUtaZS8yekRiQXB6SHhzV3JwCnpnOHJqSXFGb3owSE52aGZNNlhrTTFGZ0J4SGgvdkdIQWJCcm9NNkJqODEwcGxzbktKNmUwbVdJbGk4RHE1K3AKTUE1VjIwRVA5WHJVCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K - server: https://127.0.0.1:64749 - name: kind-dns-upstream -contexts: -- context: - cluster: kind-dns-upstream - user: kind-dns-upstream - name: kind-dns-upstream -current-context: kind-dns-upstream -kind: Config -users: -- name: kind-dns-upstream - user: - client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURLVENDQWhHZ0F3SUJBZ0lJVEl4Y21VZ1BnUFF3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TmpBeE1UTXhPVEkyTlRWYUZ3MHlOekF4TVRNeE9UTXhOVFZhTUR3eApIekFkQmdOVkJBb1RGbXQxWW1WaFpHMDZZMngxYzNSbGNpMWhaRzFwYm5NeEdUQVhCZ05WQkFNVEVHdDFZbVZ5CmJtVjBaWE10WVdSdGFXNHdnZ0VpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCRHdBd2dnRUtBb0lCQVFETy9uSHkKenN5MGFzOUlNQjFIZzk4YjJ0ZCtsSFhNVDY3aTFHTHNkcHVVMm55eWdKMFNpd25pQkpEbnVxWHJXMW45S2xxNQowSWdBVXhxbkZOTzhjQ1c3M3cvWGZhY2hiRjZEeTY5bGZHcUhMNCt2VDNiRnhqZW5aYzlOdE1tYWJSZUFZd2hDCjR3aHJvRUY4UC9Vb2trNFR1V01UZzlZRC9kem5kWlRaTTQyNzBVMnN2TVVKK3lhall0WEhCKzIwNWlTcTZ3MWwKTFNMVkxJUktRamc4Q3paUnhjVjlKK3ZWU1pNbG9xMXEzd2VCZ0lhZXRhbFN5NVpKTTJJeVAwZFVLd1diaFQ5cgozTmJ6MGV0UWdxeU4xUDc2dE9mMkNDN1ArOUtxRVlEOUgwcnpLSW5xU0JNSVA4Z2RkOUFqNnVKRWpTTjhaVXptCkZmbFdlWDRxYm5vNkh2R1RBZ01CQUFHalZqQlVNQTRHQTFVZER3RUIvd1FFQXdJRm9EQVRCZ05WSFNVRUREQUsKQmdnckJnRUZCUWNEQWpBTUJnTlZIUk1CQWY4RUFqQUFNQjhHQTFVZEl3UVlNQmFBRkd1dmY0NzlqNUF0N3BBegpVRzZyOU5XZnh2N21NQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUE5SGticjNhaXJua0lNWnRYZ0xjeUczV0ZWClhGTVdzMmtrVzVMRHBSenZMQW9lVms4Yzd3MXJBUjhhRkpoaklqaDBtZTB2ckNCL3p1UWRtNFVacjFqZkZtWlEKVWdqVG1WOWRqNm1JSWxQb2pRckFSWVNlTDY4VFlNdjMyWThRZUJhYm9lWjBQZmI5SURXTTdBVW96WmFHT25TMgpKc1RhZWkyOHdmUXkydzBZNS9rekc5bDJKUFBiRHZLRVV2czhIZTNrS1ZuNWtkMWNFcWlJU1llbnJRNS9IcGN4Cm90THl4cUtDbEdSZ3N3VmNRZDRudU1ML1JPaW1lK2RHQVQvNTlTZnpEYXZwb2tsdlN6d3IrZGNtdDV3dXB6QnYKZStKc244SjJhU1VROFBRaGFrMEdSTVlSWWlSdnN1L0JMSVdFN1FrZ0w5WkxjdnRHSkNRdW9DZnRWdUplCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K - client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBenY1eDhzN010R3JQU0RBZFI0UGZHOXJYZnBSMXpFK3U0dFJpN0hhYmxOcDhzb0NkCkVvc0o0Z1NRNTdxbDYxdFovU3BhdWRDSUFGTWFweFRUdkhBbHU5OFAxMzJuSVd4ZWc4dXZaWHhxaHkrUHIwOTIKeGNZM3AyWFBUYlRKbW0wWGdHTUlRdU1JYTZCQmZELzFLSkpPRTdsakU0UFdBLzNjNTNXVTJUT051OUZOckx6RgpDZnNtbzJMVnh3ZnR0T1lrcXVzTlpTMGkxU3lFU2tJNFBBczJVY1hGZlNmcjFVbVRKYUt0YXQ4SGdZQ0ducldwClVzdVdTVE5pTWo5SFZDc0ZtNFUvYTl6Vzg5SHJVSUtzamRUKytyVG45Z2d1ei92U3FoR0EvUjlLOHlpSjZrZ1QKQ0QvSUhYZlFJK3JpUkkwamZHVk01aFg1Vm5sK0ttNTZPaDd4a3dJREFRQUJBb0lCQUdKZWgzWjFrdERXeGFVdgp3R3BoSUNGVHNmOWt2RXFaUDZwcWRveWJuVHB6VHJsaDU4T05NZWdvZFZpNjJlanNvK3B0TzJwODBIVWZDVmFICnprd0tHOVNab0NTdmdVS2dCcGFwc0xRUkdXc2ZUakJwR2kvSkVGL01RV1ZUV2srNk1tWUFLa2ZuTHZRKzE0QWQKd1B0RDlEanBiRTAwNVB0R3BMbVdwbU5HWGIvNFVBT0lsZU9ubWpZWVVvUE50Q1JKa0pHQ2JOa3pkMnNkUDFweQpwQUVxU3F5dklONkZndnJMTnFjUFp2QnV6MnBVdFlLMFRsaUg5Yy9hbTFLdUJoSVpaSHdkVGNRZFlUczRJcU9KCnNqcHN1cURzNVcrazV6WDhKdGZ0ZUdIOFd5end3L2krdnlXc0V6cnFLZ2dPemhJWnhzSnhRNXJHeGwvSDhLVnUKcTFINUJwa0NnWUVBM3hXbzd5eXJmNGdnaENrRVRNVmMxYTdGcG1vK2ZVQkhXK0xWdFF5NEdWZHY0U0VpK1FabwpZeUd2SlYrN2ZVTjlsSnpqUVZWbVkraWhFVUI5VTZTUFQ1NmNhYXhnMFVlY1drSm0xUUJoL21sNTJWNS9mSnI3CkxiOStyYnBlcER1aVhPNXpiZC9RaVVpUURyWnVzNXAzSHMzd3hlSU5BYXhyeW1XdW4ybUVKd1VDZ1lFQTdZa0MKUUFzbDNZWFl2ODd6dFN3bWRlbVdGUUpQazE3U0x1QVdnY0daT2xTUVdvY04ycnI0V3VEazZLSXpkbkNrc1RtVAp1ekNzeTRqdlJUaUlyZjJtVTJvQ252SEJJMWxMZ1lYUkIyb1VuOGFqTURqZndtNzQ5ZFJlcVRheVRvSjkyN0lhClhXV3hpN0dDSlNWeEJOdzE0d3VFZXpaSllpTDZXNkorNWFydWFiY0NnWUFpbklHeFdnVGhySVVlL0I0bXF4aFUKTHVHTGlFQlp2bmRUMGtYRjZVdEc0MElBYzl1eE4wVkszQmNJZlduaGJXODJkNERxeWcwd3d3NzZWajhia3hTSgpEZHJHcW0vN0NGbEJ4N3Vjb0lxVHBsbTVWK2YvdFN2elZScWFhYWYxWXlzMXIrbEl5c2pZQStJVjVrZ1dwWWlGCnh2M3NOYjQrM0RsOUZYbWFVZ3ltNFFLQmdFUTUvak4zQUVGSW1LRS9TRERacFpKb3JYc0xWdC8xZEZtU2MrU0IKUHduS0VFeHdUa0p0UWJpWXNDZEJyNVp0ZEdDVE1TT3JMM2FtdGxNamtkNm41SVpCQk0reWtNOGVidG1kSGhVTApHekZwVktZZEwrZ2hCOUZVVm53MEFiTWJPQnRLWk5nK3hXaGliQWRQWWM4TGtVN05tQmZyMTlnZ1E5amVLNlM4CkhBNnhBb0dCQU5GL2hURzJVbGZRaUVYb3NkcUVUTE1jcFBYc1JORm1yV0U4Um00OVhmZzB4ajZRNWhCcFFVQnAKRGdCWXo5c213ODd0SHZzdDFtZ0drcE5qcHc4TjFRMlhiWkcxaGZtcGRuL1V4OU1NTzdQZXNoc0luUGFiL2FlawoxZ0haUjhaNFp0c2c1Ti9zTXRDOG5zQnpod0hOTnEybVRGcnoxR0ZpbUhENWxJc0V5MW5mCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg== - From f82214f8f42781412e4d2a2f4f2b6c0acc6d2f6e Mon Sep 17 00:00:00 2001 From: Zach Smith Date: Tue, 13 Jan 2026 12:07:50 -0800 Subject: [PATCH 4/4] fix: lint --- internal/controller/dnszonetsigkey_replicator_controller.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/controller/dnszonetsigkey_replicator_controller.go b/internal/controller/dnszonetsigkey_replicator_controller.go index ce44af8..ce7efc7 100644 --- a/internal/controller/dnszonetsigkey_replicator_controller.go +++ b/internal/controller/dnszonetsigkey_replicator_controller.go @@ -121,7 +121,7 @@ func (r *DNSZoneTSIGKeyReplicator) Reconcile(ctx context.Context, req mcreconcil } // Ensure downstream shadow DNSZoneTSIGKey mirrors upstream spec. - if err := r.ensureDownstreamDNSZoneTSIGKey(ctx, req.ClusterName, strategy, &upstream); err != nil { + if err := r.ensureDownstreamDNSZoneTSIGKey(ctx, strategy, &upstream); err != nil { return ctrl.Result{}, err } @@ -199,7 +199,7 @@ func (r *DNSZoneTSIGKeyReplicator) handleDeletion(ctx context.Context, c client. return true, nil } -func (r *DNSZoneTSIGKeyReplicator) ensureDownstreamDNSZoneTSIGKey(ctx context.Context, upstreamClusterName string, strategy downstreamclient.ResourceStrategy, upstream *dnsv1alpha1.DNSZoneTSIGKey) error { +func (r *DNSZoneTSIGKeyReplicator) ensureDownstreamDNSZoneTSIGKey(ctx context.Context, strategy downstreamclient.ResourceStrategy, upstream *dnsv1alpha1.DNSZoneTSIGKey) error { md, err := strategy.ObjectMetaFromUpstreamObject(ctx, upstream) if err != nil { return err