package controller import ( "context" "fmt" "sort" "time" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" bindv1alpha1 "git.unkin.net/unkin/bind-operator/api/v1alpha1" "git.unkin.net/unkin/bind-operator/internal/bind" ) const ( requeueShort = 15 * time.Second requeueLong = 2 * time.Minute managedByLabel = "app.kubernetes.io/managed-by" managedByValue = "bind-operator" clusterLabel = "bind.unkin.net/cluster" finalizer = "bind.unkin.net/finalizer" // defaultBindImage is used when BindCluster.spec.image is empty. defaultBindImage = "internetsystemsconsortium/bind9:9.20" ) func headlessServiceName(cluster string) string { return cluster + "-headless" } func primaryServiceName(cluster string) string { return cluster + "-primary" } // primaryPodSelector selects only the primary pod (ordinal 0) via the stable // StatefulSet pod-name label, for the write Service. func primaryPodSelector(cluster string) map[string]string { s := commonLabels(cluster) s["statefulset.kubernetes.io/pod-name"] = primaryPodName(cluster) return s } func clientServiceName(cluster string) string { return cluster } func primaryPodName(cluster string) string { return cluster + "-0" } func configMapName(cluster string) string { return cluster + "-config" } func keysSecretName(cluster string) string { return cluster + "-keys" } func rndcSecretName(cluster string) string { return cluster + "-rndc" } // primaryAddress is the in-cluster DNS name of the primary pod (ordinal 0). func primaryAddress(cluster, namespace string) string { return fmt.Sprintf("%s-0.%s.%s.svc.cluster.local", cluster, headlessServiceName(cluster), namespace) } // setReady sets the standard Ready condition on a status conditions slice. func setReady(conds *[]metav1.Condition, gen int64, ok bool, reason, msg string) { status := metav1.ConditionFalse if ok { status = metav1.ConditionTrue } meta.SetStatusCondition(conds, metav1.Condition{ Type: "Ready", Status: status, Reason: reason, Message: msg, ObservedGeneration: gen, }) } // commonLabels are applied to every object the operator creates for a cluster. func commonLabels(cluster string) map[string]string { return map[string]string{ managedByLabel: managedByValue, clusterLabel: cluster, } } // getCluster fetches the BindCluster referenced by clusterRef in namespace. func getCluster(ctx context.Context, c client.Client, namespace, clusterRef string) (*bindv1alpha1.BindCluster, error) { var cluster bindv1alpha1.BindCluster if err := c.Get(ctx, client.ObjectKey{Namespace: namespace, Name: clusterRef}, &cluster); err != nil { return nil, err } return &cluster, nil } // primaryReady reports whether the primary pod of a cluster is Ready. func primaryReady(ctx context.Context, c client.Client, cluster *bindv1alpha1.BindCluster) bool { var pod corev1.Pod key := client.ObjectKey{Namespace: cluster.Namespace, Name: primaryPodName(cluster.Name)} if err := c.Get(ctx, key, &pod); err != nil { return false } for _, cond := range pod.Status.Conditions { if cond.Type == corev1.PodReady { return cond.Status == corev1.ConditionTrue } } return false } // primaryPodIP returns the pod IP of a cluster's primary pod (ordinal 0), or an // empty string if the pod has no IP yet. BIND's primaries/default-primaries // only accept IP addresses (not hostnames), and zone seeding needs the address // for glue, so the operator resolves the pod IP rather than using a DNS name. func primaryPodIP(ctx context.Context, c client.Client, cluster *bindv1alpha1.BindCluster) string { var pod corev1.Pod if err := c.Get(ctx, client.ObjectKey{Namespace: cluster.Namespace, Name: primaryPodName(cluster.Name)}, &pod); err != nil { return "" } return pod.Status.PodIP } // primaryTransferAddress returns the address secondaries use to reach the // primary for catalog and zone AXFR. It prefers the primary Service ClusterIP, // which is stable across primary pod restarts (the pod IP is not: it changes on // every restart, leaving secondaries pointed at a dead address). It falls back // to the primary pod IP when no primary Service is configured or its ClusterIP // is not yet assigned. func primaryTransferAddress(ctx context.Context, c client.Client, cluster *bindv1alpha1.BindCluster) string { if cluster.Spec.PrimaryService != nil { var svc corev1.Service if err := c.Get(ctx, client.ObjectKey{Namespace: cluster.Namespace, Name: primaryServiceName(cluster.Name)}, &svc); err == nil { if ip := svc.Spec.ClusterIP; ip != "" && ip != corev1.ClusterIPNone { return ip } } } return primaryPodIP(ctx, c, cluster) } // secondaryPodIPs returns the pod IPs of a cluster's secondary pods (every pod // except the ordinal-0 primary) that currently have an address. The primary // uses this list as its zone `also-notify` set, so a change to a primary zone // (in particular a dynamic update) triggers an immediate NOTIFY -> IXFR to the // secondaries instead of leaving them stale until the next SOA refresh. The // list is sorted so the rendered zone config is stable and does not churn // modzone on every reconcile. Pod IPs change across restarts, so the caller // relies on the zone controller's periodic requeue to refresh the set (a // restarted secondary re-transfers the whole zone on load regardless). Returns // nil for a single-replica cluster. func secondaryPodIPs(ctx context.Context, c client.Client, cluster *bindv1alpha1.BindCluster) []string { var pods corev1.PodList if err := c.List(ctx, &pods, client.InNamespace(cluster.Namespace), client.MatchingLabels(commonLabels(cluster.Name))); err != nil { return nil } primary := primaryPodName(cluster.Name) var ips []string for i := range pods.Items { p := &pods.Items[i] if p.Name == primary || p.Status.PodIP == "" { continue } ips = append(ips, p.Status.PodIP) } sort.Strings(ips) return ips } // resolveTSIG reads the material of a BindTSIGKey into TSIG credentials. func resolveTSIG(ctx context.Context, c client.Client, namespace, keyRef string) (bind.TSIGCreds, error) { var creds bind.TSIGCreds if keyRef == "" { return creds, fmt.Errorf("no TSIG key referenced") } var key bindv1alpha1.BindTSIGKey if err := c.Get(ctx, client.ObjectKey{Namespace: namespace, Name: keyRef}, &key); err != nil { return creds, fmt.Errorf("get tsig key %s: %w", keyRef, err) } secretName := key.Status.SecretName if secretName == "" { secretName = key.Spec.SecretName } if secretName == "" { secretName = keyRef + "-tsig" } var secret corev1.Secret if err := c.Get(ctx, client.ObjectKey{Namespace: namespace, Name: secretName}, &secret); err != nil { return creds, fmt.Errorf("get tsig secret %s: %w", secretName, err) } keyName := key.Spec.KeyName if keyName == "" { keyName = keyRef } creds = bind.TSIGCreds{ Name: keyName, Algorithm: string(secret.Data["algorithm"]), Secret: string(secret.Data["secret"]), } if creds.Algorithm == "" { creds.Algorithm = string(bindv1alpha1.TSIGHMACSHA256) } return creds, nil }