Files
bind-operator/internal/controller/bindtsigkey_controller.go
T
unkinben 9c81320df8 BindTSIGKey: add secretTemplate for labels/annotations on the managed Secret
The operator-generated TSIG Secret previously carried only the managed-by
label, so it could not be mirrored to another namespace by emberstack
reflector (which requires reflection-allowed annotations on the source).

Add spec.secretTemplate.{annotations,labels}, applied both when the Secret
is first generated and reconciled onto the existing Secret when the CR
changes (imported secrets are left untouched so we don't fight their
external manager). This lets the external-dns TSIG key be managed in
bind-internal and reflected into the externaldns namespace.
2026-07-20 23:45:41 +10:00

161 lines
5.1 KiB
Go

package controller
import (
"context"
"fmt"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
bindv1alpha1 "git.unkin.net/unkin/bind-operator/api/v1alpha1"
"git.unkin.net/unkin/bind-operator/internal/bind"
)
// BindTSIGKeyReconciler generates TSIG key material into a Secret.
type BindTSIGKeyReconciler struct {
client.Client
Scheme *runtime.Scheme
}
// +kubebuilder:rbac:groups=bind.unkin.net,resources=bindtsigkeys,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=bind.unkin.net,resources=bindtsigkeys/status,verbs=get;update;patch
// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete
func (r *BindTSIGKeyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.FromContext(ctx)
var key bindv1alpha1.BindTSIGKey
if err := r.Get(ctx, req.NamespacedName, &key); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
algorithm := string(key.Spec.Algorithm)
if algorithm == "" {
algorithm = string(bindv1alpha1.TSIGHMACSHA256)
}
keyName := key.Spec.KeyName
if keyName == "" {
keyName = key.Name
}
secretName := key.Spec.SecretName
if secretName == "" {
secretName = key.Name + "-tsig"
}
var secret corev1.Secret
err := r.Get(ctx, types.NamespacedName{Namespace: key.Namespace, Name: secretName}, &secret)
switch {
case apierrors.IsNotFound(err):
if key.Spec.ImportExisting {
return r.fail(ctx, &key, "SecretMissing", fmt.Sprintf("import secret %s not found", secretName))
}
material, genErr := bind.GenerateSecret(bind.SecretBytesForAlgorithm(algorithm))
if genErr != nil {
return ctrl.Result{}, genErr
}
newSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: secretName, Namespace: key.Namespace},
Data: map[string][]byte{
"algorithm": []byte(algorithm),
"keyName": []byte(keyName),
"secret": []byte(material),
"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
}
if err := r.Create(ctx, newSecret); err != nil {
return ctrl.Result{}, err
}
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
key.Status.KeyName = keyName
key.Status.Ready = true
key.Status.ObservedGeneration = key.Generation
setReady(&key.Status.Conditions, key.Generation, true, "KeyReady", "TSIG key material present")
if err := r.Status().Update(ctx, &key); err != nil {
return ctrl.Result{}, err
}
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
setReady(&key.Status.Conditions, key.Generation, false, reason, msg)
if err := r.Status().Update(ctx, key); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{RequeueAfter: requeueShort}, nil
}
func (r *BindTSIGKeyReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&bindv1alpha1.BindTSIGKey{}).
Owns(&corev1.Secret{}).
Complete(r)
}