9c81320df8
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.
161 lines
5.1 KiB
Go
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)
|
|
}
|