package tsigapi import ( "context" "fmt" "time" 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/types" "sigs.k8s.io/controller-runtime/pkg/client" bindv1alpha1 "git.unkin.net/unkin/bind-operator/api/v1alpha1" ) // createKey creates a BindTSIGKey and waits for the operator to materialise it. func (s *Server) createKey(ctx context.Context, req createRequest) (*keyResponse, error) { algorithm := req.Algorithm if algorithm == "" { algorithm = string(bindv1alpha1.TSIGHMACSHA256) } key := &bindv1alpha1.BindTSIGKey{ ObjectMeta: metav1.ObjectMeta{ Name: req.Name, Namespace: s.Namespace, Labels: map[string]string{"app.kubernetes.io/managed-by": "bind-tsig-api"}, }, Spec: bindv1alpha1.BindTSIGKeySpec{ Algorithm: bindv1alpha1.TSIGAlgorithm(algorithm), ClusterRef: req.ClusterRef, KeyName: req.Name, }, } if err := s.Client.Create(ctx, key); err != nil && !apierrors.IsAlreadyExists(err) { return nil, fmt.Errorf("create bindtsigkey: %w", err) } return s.waitForMaterial(ctx, req.Name) } // readKey returns the current material for a key. func (s *Server) readKey(ctx context.Context, name string) (*keyResponse, error) { var key bindv1alpha1.BindTSIGKey if err := s.Client.Get(ctx, s.key(name), &key); err != nil { return nil, err } return s.material(ctx, &key) } // rotateKey replaces the key material: it deletes the key Secret (the operator // regenerates it) and bumps a rotation annotation so the cluster controller // re-renders keys.conf and reloads BIND. func (s *Server) rotateKey(ctx context.Context, name string) (*keyResponse, error) { var key bindv1alpha1.BindTSIGKey if err := s.Client.Get(ctx, s.key(name), &key); err != nil { return nil, err } secretName := secretNameFor(&key) var secret corev1.Secret if err := s.Client.Get(ctx, client.ObjectKey{Namespace: s.Namespace, Name: secretName}, &secret); err == nil { if derr := s.Client.Delete(ctx, &secret); derr != nil && !apierrors.IsNotFound(derr) { return nil, fmt.Errorf("delete key secret: %w", derr) } } else if !apierrors.IsNotFound(err) { return nil, err } // Trigger re-generation + re-render by bumping the CR. if key.Annotations == nil { key.Annotations = map[string]string{} } key.Annotations[rotationAnnotation] = s.Now().UTC().Format(time.RFC3339Nano) if err := s.Client.Update(ctx, &key); err != nil { return nil, fmt.Errorf("bump rotation annotation: %w", err) } return s.waitForNewMaterial(ctx, name, secret.UID) } // deleteKey removes the BindTSIGKey (its Secret is garbage-collected). func (s *Server) deleteKey(ctx context.Context, name string) error { key := &bindv1alpha1.BindTSIGKey{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: s.Namespace}} if err := s.Client.Delete(ctx, key); err != nil && !apierrors.IsNotFound(err) { return err } return nil } // material reads the key material from the Secret referenced by a BindTSIGKey. func (s *Server) material(ctx context.Context, key *bindv1alpha1.BindTSIGKey) (*keyResponse, error) { var secret corev1.Secret if err := s.Client.Get(ctx, client.ObjectKey{Namespace: s.Namespace, Name: secretNameFor(key)}, &secret); err != nil { return nil, err } algorithm := string(secret.Data["algorithm"]) if algorithm == "" { algorithm = string(key.Spec.Algorithm) } keyName := string(secret.Data["keyName"]) if keyName == "" { keyName = key.Name } return &keyResponse{ Name: key.Name, Algorithm: algorithm, Secret: string(secret.Data["secret"]), KeyName: keyName, ClusterRef: key.Spec.ClusterRef, }, nil } // waitForMaterial polls until the key's Secret exists (operator reconciled). func (s *Server) waitForMaterial(ctx context.Context, name string) (*keyResponse, error) { deadline := s.Now().Add(s.WaitTimeout) for { var key bindv1alpha1.BindTSIGKey if err := s.Client.Get(ctx, s.key(name), &key); err != nil { return nil, err } if resp, err := s.material(ctx, &key); err == nil && resp.Secret != "" { return resp, nil } if s.Now().After(deadline) { return nil, fmt.Errorf("timed out waiting for key %q material", name) } select { case <-ctx.Done(): return nil, ctx.Err() case <-time.After(250 * time.Millisecond): } } } // waitForNewMaterial polls until the Secret has been recreated (different UID). func (s *Server) waitForNewMaterial(ctx context.Context, name string, oldUID types.UID) (*keyResponse, error) { deadline := s.Now().Add(s.WaitTimeout) for { var key bindv1alpha1.BindTSIGKey if err := s.Client.Get(ctx, s.key(name), &key); err != nil { return nil, err } var secret corev1.Secret err := s.Client.Get(ctx, client.ObjectKey{Namespace: s.Namespace, Name: secretNameFor(&key)}, &secret) if err == nil && secret.UID != oldUID && len(secret.Data["secret"]) > 0 { return s.material(ctx, &key) } if s.Now().After(deadline) { return nil, fmt.Errorf("timed out waiting for key %q to rotate", name) } select { case <-ctx.Done(): return nil, ctx.Err() case <-time.After(250 * time.Millisecond): } } } // secretNameFor returns the Secret name holding a key's material. func secretNameFor(key *bindv1alpha1.BindTSIGKey) string { if key.Status.SecretName != "" { return key.Status.SecretName } if key.Spec.SecretName != "" { return key.Spec.SecretName } return key.Name + "-tsig" }