2deea3e023
Pods copy config from the projected volume into an emptyDir once at startup, so a ConfigMap or keys.conf change never reaches a running pod: rndc reconfig re-reads the stale startup copy, and a manual `kubectl rollout restart` is reverted because the operator overwrites the pod template every reconcile. The only thing that applies new config is a restart, and nothing triggered one. Stamp a hash of the projected config (rendered ConfigMap + keys.conf Secret) onto the pod template as bind.unkin.net/config-hash. When config changes the hash flips, the template changes, and the StatefulSet does a normal rolling restart so every pod re-copies fresh config. The operator owns the template, so the restart is operator-driven and not reverted; a stable hash means no spurious restarts. Covers named.conf changes (ACLs, views, forwarders, validate-except, primary address) and TSIG key rotation.
65 lines
2.1 KiB
Go
65 lines
2.1 KiB
Go
package controller
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
|
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
|
|
|
bindv1alpha1 "git.unkin.net/unkin/bind-operator/api/v1alpha1"
|
|
)
|
|
|
|
func TestConfigHashStableAndSensitive(t *testing.T) {
|
|
scheme := runtime.NewScheme()
|
|
if err := clientgoscheme.AddToScheme(scheme); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := bindv1alpha1.AddToScheme(scheme); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cluster := &bindv1alpha1.BindCluster{ObjectMeta: metav1.ObjectMeta{Name: "c", Namespace: "ns"}}
|
|
cm := &corev1.ConfigMap{
|
|
ObjectMeta: metav1.ObjectMeta{Name: configMapName("c"), Namespace: "ns"},
|
|
Data: map[string]string{"named.conf.secondary": "options { recursion yes; };"},
|
|
}
|
|
keys := &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{Name: keysSecretName("c"), Namespace: "ns"},
|
|
Data: map[string][]byte{"keys.conf": []byte("key x {};")},
|
|
}
|
|
c := fake.NewClientBuilder().WithScheme(scheme).WithObjects(cm, keys).Build()
|
|
r := &BindClusterReconciler{Client: c, Scheme: scheme}
|
|
ctx := context.Background()
|
|
|
|
h1 := r.configHash(ctx, cluster)
|
|
if h1 == "" {
|
|
t.Fatal("hash should not be empty when config exists")
|
|
}
|
|
// Stable across calls when nothing changes.
|
|
if h2 := r.configHash(ctx, cluster); h2 != h1 {
|
|
t.Fatalf("hash not stable: %s != %s", h1, h2)
|
|
}
|
|
|
|
// A config change flips the hash (this is what rolls the StatefulSet).
|
|
cm.Data["named.conf.secondary"] = "options { recursion yes; validate-except { unkin.net; }; };"
|
|
if err := c.Update(ctx, cm); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if h3 := r.configHash(ctx, cluster); h3 == h1 {
|
|
t.Fatal("hash must change when the ConfigMap changes")
|
|
}
|
|
|
|
// A TSIG key (keys.conf) change also flips it.
|
|
afterCM := r.configHash(ctx, cluster)
|
|
keys.Data["keys.conf"] = []byte("key x { algorithm hmac-sha256; };")
|
|
if err := c.Update(ctx, keys); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if h4 := r.configHash(ctx, cluster); h4 == afterCM {
|
|
t.Fatal("hash must change when keys.conf changes")
|
|
}
|
|
}
|