Merge pull request 'BindTSIGKey: add secretTemplate for Secret labels/annotations' (#12) from benvin/tsigkey-secret-annotations into main
Reviewed-on: #12
This commit was merged in pull request #12.
This commit is contained in:
@@ -42,7 +42,7 @@ script picks one based on the pod ordinal.
|
||||
| `BindZone` | A forward/reverse zone (`primary`/`secondary`/`forward`/`stub`), records inline, optional dynamic-update + DNSSEC + catalog membership. |
|
||||
| `DNSRecord` | A single record set applied via TSIG `nsupdate` — external-dns as a CRD. |
|
||||
| `BindView` | A split-horizon view (`match-clients`, ordering, per-view recursion). |
|
||||
| `BindTSIGKey` | A TSIG key; the operator generates material into a Secret (never stored in the CR). |
|
||||
| `BindTSIGKey` | A TSIG key; the operator generates material into a Secret (never stored in the CR). `spec.secretTemplate` stamps extra labels/annotations onto that Secret (e.g. reflection hints to mirror it into another namespace). |
|
||||
| `BindACL` | A reusable named `address_match_list`. |
|
||||
| `BindCatalogZone` | A BIND catalog zone so secondaries auto-provision member zones. |
|
||||
| `BindPolicy` | A Response Policy Zone (RPZ) / DNS firewall. |
|
||||
|
||||
@@ -41,6 +41,24 @@ type BindTSIGKeySpec struct {
|
||||
// `secret` key and the operator will not generate new material.
|
||||
// +optional
|
||||
ImportExisting bool `json:"importExisting,omitempty"`
|
||||
|
||||
// SecretTemplate customizes metadata written onto the managed key Secret.
|
||||
// Useful, for example, to let secret-reflection tooling mirror the key into
|
||||
// another namespace. Operator-managed labels are always preserved.
|
||||
// +optional
|
||||
SecretTemplate *SecretMetadata `json:"secretTemplate,omitempty"`
|
||||
}
|
||||
|
||||
// SecretMetadata carries extra labels and annotations to stamp onto a
|
||||
// Secret managed by the operator.
|
||||
type SecretMetadata struct {
|
||||
// Annotations to set on the Secret.
|
||||
// +optional
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
|
||||
// Labels to set on the Secret.
|
||||
// +optional
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
}
|
||||
|
||||
// BindTSIGKeyStatus reports observed TSIG key state.
|
||||
|
||||
@@ -691,7 +691,7 @@ func (in *BindTSIGKey) DeepCopyInto(out *BindTSIGKey) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
out.Spec = in.Spec
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
@@ -748,6 +748,11 @@ func (in *BindTSIGKeyList) DeepCopyObject() runtime.Object {
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *BindTSIGKeySpec) DeepCopyInto(out *BindTSIGKeySpec) {
|
||||
*out = *in
|
||||
if in.SecretTemplate != nil {
|
||||
in, out := &in.SecretTemplate, &out.SecretTemplate
|
||||
*out = new(SecretMetadata)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BindTSIGKeySpec.
|
||||
@@ -1208,3 +1213,32 @@ func (in *Record) DeepCopy() *Record {
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SecretMetadata) DeepCopyInto(out *SecretMetadata) {
|
||||
*out = *in
|
||||
if in.Annotations != nil {
|
||||
in, out := &in.Annotations, &out.Annotations
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
if in.Labels != nil {
|
||||
in, out := &in.Labels, &out.Labels
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretMetadata.
|
||||
func (in *SecretMetadata) DeepCopy() *SecretMetadata {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(SecretMetadata)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -87,6 +87,23 @@ spec:
|
||||
SecretName is the Secret the key material is written to (or read from when
|
||||
ImportExisting is set). Defaults to "<name>-tsig".
|
||||
type: string
|
||||
secretTemplate:
|
||||
description: |-
|
||||
SecretTemplate customizes metadata written onto the managed key Secret.
|
||||
Useful, for example, to let secret-reflection tooling mirror the key into
|
||||
another namespace. Operator-managed labels are always preserved.
|
||||
properties:
|
||||
annotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: Annotations to set on the Secret.
|
||||
type: object
|
||||
labels:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: Labels to set on the Secret.
|
||||
type: object
|
||||
type: object
|
||||
type: object
|
||||
status:
|
||||
description: BindTSIGKeyStatus reports observed TSIG key state.
|
||||
|
||||
@@ -2376,6 +2376,23 @@ spec:
|
||||
SecretName is the Secret the key material is written to (or read from when
|
||||
ImportExisting is set). Defaults to "<name>-tsig".
|
||||
type: string
|
||||
secretTemplate:
|
||||
description: |-
|
||||
SecretTemplate customizes metadata written onto the managed key Secret.
|
||||
Useful, for example, to let secret-reflection tooling mirror the key into
|
||||
another namespace. Operator-managed labels are always preserved.
|
||||
properties:
|
||||
annotations:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: Annotations to set on the Secret.
|
||||
type: object
|
||||
labels:
|
||||
additionalProperties:
|
||||
type: string
|
||||
description: Labels to set on the Secret.
|
||||
type: object
|
||||
type: object
|
||||
type: object
|
||||
status:
|
||||
description: BindTSIGKeyStatus reports observed TSIG key state.
|
||||
|
||||
@@ -11,7 +11,9 @@ spec:
|
||||
algorithm: hmac-sha256
|
||||
---
|
||||
# TSIG key permitting external-dns (and DNSRecord objects) to send RFC2136
|
||||
# dynamic updates to the dynamic cluster's primary.
|
||||
# dynamic updates to the dynamic cluster's primary. secretTemplate mirrors the
|
||||
# generated Secret into the external-dns namespace via emberstack reflector, so
|
||||
# external-dns presents exactly the key the primary's allow-update accepts.
|
||||
apiVersion: bind.unkin.net/v1alpha1
|
||||
kind: BindTSIGKey
|
||||
metadata:
|
||||
@@ -19,3 +21,9 @@ metadata:
|
||||
namespace: bind-externaldns
|
||||
spec:
|
||||
algorithm: hmac-sha256
|
||||
secretTemplate:
|
||||
annotations:
|
||||
reflector.v1.k8s.emberstack.com/reflection-allowed: "true"
|
||||
reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces: "externaldns"
|
||||
reflector.v1.k8s.emberstack.com/reflection-auto-enabled: "true"
|
||||
reflector.v1.k8s.emberstack.com/reflection-auto-namespaces: "externaldns"
|
||||
|
||||
@@ -3,6 +3,7 @@ module git.unkin.net/unkin/bind-operator
|
||||
go 1.25
|
||||
|
||||
require (
|
||||
github.com/go-logr/logr v1.4.2
|
||||
k8s.io/api v0.34.4
|
||||
k8s.io/apimachinery v0.34.4
|
||||
k8s.io/client-go v0.34.4
|
||||
@@ -17,7 +18,6 @@ require (
|
||||
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/zapr v1.3.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
|
||||
@@ -60,7 +60,7 @@ func (r *BindTSIGKeyReconciler) Reconcile(ctx context.Context, req ctrl.Request)
|
||||
return ctrl.Result{}, genErr
|
||||
}
|
||||
newSecret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: secretName, Namespace: key.Namespace, Labels: map[string]string{managedByLabel: managedByValue}},
|
||||
ObjectMeta: metav1.ObjectMeta{Name: secretName, Namespace: key.Namespace},
|
||||
Data: map[string][]byte{
|
||||
"algorithm": []byte(algorithm),
|
||||
"keyName": []byte(keyName),
|
||||
@@ -68,6 +68,7 @@ func (r *BindTSIGKeyReconciler) Reconcile(ctx context.Context, req ctrl.Request)
|
||||
"key.conf": []byte(bind.KeyClause(keyName, algorithm, material)),
|
||||
},
|
||||
}
|
||||
applySecretTemplate(&newSecret.ObjectMeta, key.Spec.SecretTemplate)
|
||||
if err := ctrl.SetControllerReference(&key, newSecret, r.Scheme); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
@@ -77,6 +78,21 @@ func (r *BindTSIGKeyReconciler) Reconcile(ctx context.Context, req ctrl.Request)
|
||||
logger.Info("generated TSIG key", "key", key.Name, "secret", secretName)
|
||||
case err != nil:
|
||||
return ctrl.Result{}, err
|
||||
default:
|
||||
// Secret already exists: reconcile the template-managed metadata so that
|
||||
// annotation/label changes on the CR (e.g. reflection hints) propagate
|
||||
// without regenerating key material. Skip imported secrets, which are
|
||||
// owned by an external manager (Vault/VSO, reflector) that we must not
|
||||
// fight over metadata.
|
||||
if key.Spec.ImportExisting {
|
||||
break
|
||||
}
|
||||
if updated := applySecretTemplate(&secret.ObjectMeta, key.Spec.SecretTemplate); updated {
|
||||
if err := r.Update(ctx, &secret); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
logger.Info("updated TSIG key secret metadata", "key", key.Name, "secret", secretName)
|
||||
}
|
||||
}
|
||||
|
||||
key.Status.SecretName = secretName
|
||||
@@ -90,6 +106,42 @@ func (r *BindTSIGKeyReconciler) Reconcile(ctx context.Context, req ctrl.Request)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// applySecretTemplate stamps the operator-managed label plus any
|
||||
// user-supplied labels/annotations onto the Secret's metadata. It returns true
|
||||
// if it mutated meta, so callers can decide whether an update is needed.
|
||||
func applySecretTemplate(meta *metav1.ObjectMeta, tmpl *bindv1alpha1.SecretMetadata) bool {
|
||||
changed := false
|
||||
setLabel := func(k, v string) {
|
||||
if meta.Labels == nil {
|
||||
meta.Labels = map[string]string{}
|
||||
}
|
||||
if meta.Labels[k] != v {
|
||||
meta.Labels[k] = v
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
setAnnotation := func(k, v string) {
|
||||
if meta.Annotations == nil {
|
||||
meta.Annotations = map[string]string{}
|
||||
}
|
||||
if meta.Annotations[k] != v {
|
||||
meta.Annotations[k] = v
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
|
||||
setLabel(managedByLabel, managedByValue)
|
||||
if tmpl != nil {
|
||||
for k, v := range tmpl.Labels {
|
||||
setLabel(k, v)
|
||||
}
|
||||
for k, v := range tmpl.Annotations {
|
||||
setAnnotation(k, v)
|
||||
}
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
func (r *BindTSIGKeyReconciler) fail(ctx context.Context, key *bindv1alpha1.BindTSIGKey, reason, msg string) (ctrl.Result, error) {
|
||||
key.Status.Ready = false
|
||||
key.Status.ObservedGeneration = key.Generation
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
bindv1alpha1 "git.unkin.net/unkin/bind-operator/api/v1alpha1"
|
||||
)
|
||||
|
||||
func TestApplySecretTemplate(t *testing.T) {
|
||||
t.Run("nil template still stamps managed-by label", func(t *testing.T) {
|
||||
var meta metav1.ObjectMeta
|
||||
if !applySecretTemplate(&meta, nil) {
|
||||
t.Fatal("expected change on empty meta")
|
||||
}
|
||||
if meta.Labels[managedByLabel] != managedByValue {
|
||||
t.Errorf("managed-by label = %q, want %q", meta.Labels[managedByLabel], managedByValue)
|
||||
}
|
||||
if meta.Annotations != nil {
|
||||
t.Errorf("annotations = %v, want nil", meta.Annotations)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("applies labels and annotations", func(t *testing.T) {
|
||||
meta := metav1.ObjectMeta{Labels: map[string]string{managedByLabel: managedByValue}}
|
||||
tmpl := &bindv1alpha1.SecretMetadata{
|
||||
Annotations: map[string]string{"reflector.v1.k8s.emberstack.com/reflection-allowed": "true"},
|
||||
Labels: map[string]string{"team": "dns"},
|
||||
}
|
||||
if !applySecretTemplate(&meta, tmpl) {
|
||||
t.Fatal("expected change when adding template metadata")
|
||||
}
|
||||
if got := meta.Annotations["reflector.v1.k8s.emberstack.com/reflection-allowed"]; got != "true" {
|
||||
t.Errorf("reflection annotation = %q, want true", got)
|
||||
}
|
||||
if meta.Labels["team"] != "dns" {
|
||||
t.Errorf("team label = %q, want dns", meta.Labels["team"])
|
||||
}
|
||||
// managed-by must survive user-supplied labels.
|
||||
if meta.Labels[managedByLabel] != managedByValue {
|
||||
t.Errorf("managed-by label dropped: %v", meta.Labels)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("idempotent when already applied", func(t *testing.T) {
|
||||
tmpl := &bindv1alpha1.SecretMetadata{
|
||||
Annotations: map[string]string{"a": "1"},
|
||||
Labels: map[string]string{"b": "2"},
|
||||
}
|
||||
meta := metav1.ObjectMeta{}
|
||||
applySecretTemplate(&meta, tmpl)
|
||||
if applySecretTemplate(&meta, tmpl) {
|
||||
t.Error("expected no change on second apply")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("updates drifted annotation value", func(t *testing.T) {
|
||||
meta := metav1.ObjectMeta{
|
||||
Labels: map[string]string{managedByLabel: managedByValue},
|
||||
Annotations: map[string]string{"a": "old"},
|
||||
}
|
||||
tmpl := &bindv1alpha1.SecretMetadata{Annotations: map[string]string{"a": "new"}}
|
||||
if !applySecretTemplate(&meta, tmpl) {
|
||||
t.Fatal("expected change when annotation value drifts")
|
||||
}
|
||||
if meta.Annotations["a"] != "new" {
|
||||
t.Errorf("annotation a = %q, want new", meta.Annotations["a"])
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user