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) }