Target upstream ISC bind9 image

Uses internetsystemsconsortium/bind9 as the default base image instead of
a self-hosted one, verified against internetsystemsconsortium/bind9:9.20
(runs as root; named/rndc/nsupdate at /usr/sbin,/usr/sbin,/usr/bin).

- project operator config at /etc/bind-operator instead of overmounting
  the image's /etc/bind (keeps bind.keys / base config intact)
- reference named/rndc/nsupdate by absolute path (exec PATH may exclude
  /usr/sbin)
- centralise filesystem + binary paths in internal/bind/consts.go
- default spec.image to internetsystemsconsortium/bind9:9.20
This commit is contained in:
2026-07-03 17:41:13 +10:00
parent fe5fbdaf6d
commit 4092a25f4f
11 changed files with 66 additions and 29 deletions
+4 -2
View File
@@ -90,8 +90,10 @@ on pull requests; pushing a `v*` tag builds and pushes
## Notes & caveats
- The BIND container image (`spec.image`, default
`git.unkin.net/unkin/bind9:latest`) must ship `named`, `rndc` and `nsupdate`,
read `/run/named/named.conf`, and honour the operator's `/etc/bind` layout.
`internetsystemsconsortium/bind9:9.20`) must ship `named`, `rndc` and
`nsupdate`. The operator projects its config at `/etc/bind-operator` (leaving
the image's own `/etc/bind`, including `bind.keys`, intact) and runs
`named -g -c /run/named/named.conf`.
- Dynamic updates authenticate with `nsupdate -y`; the TSIG secret is passed on
the argv of an exec'd process inside the pod.
- RPZ IP-trigger encodings (`ip`, `client-ip`, `nsip`) are emitted verbatim;
+2 -2
View File
@@ -52,8 +52,8 @@ type BindClusterSpec struct {
// +optional
Replicas int32 `json:"replicas,omitempty"`
// Image is the BIND9 container image.
// +kubebuilder:default="git.unkin.net/unkin/bind9:latest"
// Image is the BIND9 container image. Must ship named, rndc and nsupdate.
// +kubebuilder:default="internetsystemsconsortium/bind9:9.20"
// +optional
Image string `json:"image,omitempty"`
@@ -994,8 +994,9 @@ spec:
type: string
type: array
image:
default: git.unkin.net/unkin/bind9:latest
description: Image is the BIND9 container image.
default: internetsystemsconsortium/bind9:9.20
description: Image is the BIND9 container image. Must ship named,
rndc and nsupdate.
type: string
imagePullPolicy:
description: ImagePullPolicy for the BIND container.
+37
View File
@@ -0,0 +1,37 @@
package bind
// Filesystem layout and binary locations inside a BIND pod. The operator mounts
// its rendered config at ConfigDir (a path distinct from the image's own
// /etc/bind, so the base image's bind.keys trust anchors remain available for
// dnssec-validation).
const (
// ContainerName is the BIND container name within each pod.
ContainerName = "bind"
// ConfigDir is where the operator projects named.conf, keys and the
// entrypoint. Kept separate from the image's /etc/bind.
ConfigDir = "/etc/bind-operator"
// DataDir is BIND's writable working directory (backed by the PVC): zone
// databases and journals.
DataDir = "/var/lib/named"
// RunDir holds the ordinal-selected named.conf (writable emptyDir).
RunDir = "/run/named"
// Binary locations in the ISC BIND9 image (Debian/Ubuntu layout).
NamedBin = "/usr/sbin/named"
RndcBin = "/usr/sbin/rndc"
NsupdateBin = "/usr/bin/nsupdate"
)
// Config file paths derived from ConfigDir.
const (
NamedConfRun = RunDir + "/named.conf"
NamedConfPrimary = ConfigDir + "/named.conf.primary"
NamedConfSecondary = ConfigDir + "/named.conf.secondary"
EntrypointPath = ConfigDir + "/entrypoint.sh"
KeysConfPath = ConfigDir + "/keys.conf"
RndcKeyPath = ConfigDir + "/rndc.key"
RndcConfPath = ConfigDir + "/rndc.conf"
)
-3
View File
@@ -14,9 +14,6 @@ import (
"k8s.io/client-go/tools/remotecommand"
)
// ContainerName is the BIND container name within each pod.
const ContainerName = "bind"
// Executor runs commands inside BIND pods via the exec subresource.
type Executor struct {
config *rest.Config
+1 -1
View File
@@ -41,7 +41,7 @@ func (e *Executor) NSUpdate(ctx context.Context, namespace, pod, zone string, cr
}
b.WriteString("send\n")
cmd := []string{"nsupdate", "-y", fmt.Sprintf("%s:%s:%s", creds.Algorithm, creds.Name, creds.Secret)}
cmd := []string{NsupdateBin, "-y", fmt.Sprintf("%s:%s:%s", creds.Algorithm, creds.Name, creds.Secret)}
if out, err := e.Exec(ctx, namespace, pod, cmd, b.String()); err != nil {
return fmt.Errorf("nsupdate zone %s: %w (out: %s)", zone, err, out)
}
+2 -4
View File
@@ -20,9 +20,6 @@ type RenderInput struct {
PrimaryAddress string
}
// DataDir is where BIND keeps zone databases and journals (backed by the PVC).
const DataDir = "/var/lib/named"
// RenderNamedConf returns the primary and secondary named.conf contents for a
// cluster. Both variants are shipped in the ConfigMap; the entrypoint selects
// one based on the pod ordinal.
@@ -35,7 +32,8 @@ func render(in RenderInput, isPrimary bool) string {
var b strings.Builder
b.WriteString("// Managed by bind-operator. Do not edit.\n")
b.WriteString(`include "/etc/bind/keys/keys.conf";` + "\n\n")
b.WriteString(fmt.Sprintf("include \"%s\";\n", RndcKeyPath))
b.WriteString(fmt.Sprintf("include \"%s\";\n\n", KeysConfPath))
// Named ACLs (global scope).
acls := append([]bindv1alpha1.BindACL(nil), in.ACLs...)
+1 -4
View File
@@ -6,12 +6,9 @@ import (
"strings"
)
// RndcConfPath is the operator-managed rndc client config mounted in each pod.
const RndcConfPath = "/etc/bind/rndc.conf"
// Rndc runs `rndc <args...>` on a pod and returns its output.
func (e *Executor) Rndc(ctx context.Context, namespace, pod string, args ...string) (string, error) {
base := []string{"rndc", "-c", RndcConfPath}
base := []string{RndcBin, "-c", RndcConfPath}
return e.Exec(ctx, namespace, pod, append(base, args...), "")
}
@@ -109,7 +109,7 @@ func (r *BindClusterReconciler) reconcileRNDCSecret(ctx context.Context, c *bind
return genErr
}
keyClause := bind.KeyClause("rndc-key", "hmac-sha256", secret)
rndcConf := fmt.Sprintf("include \"/etc/bind/rndc.key\";\noptions {\n default-key \"rndc-key\";\n default-server 127.0.0.1;\n default-port 953;\n};\n")
rndcConf := fmt.Sprintf("include \"%s\";\noptions {\n default-key \"rndc-key\";\n default-server 127.0.0.1;\n default-port 953;\n};\n", bind.RndcKeyPath)
s := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: c.Namespace, Labels: commonLabels(c.Name)},
Data: map[string][]byte{
@@ -259,7 +259,7 @@ func (r *BindClusterReconciler) reconcileStatefulSet(ctx context.Context, c *bin
replicas := c.Spec.Replicas
image := c.Spec.Image
if image == "" {
image = "git.unkin.net/unkin/bind9:latest"
image = defaultBindImage
}
storageSize := c.Spec.StorageSize
if storageSize == "" {
@@ -295,15 +295,15 @@ func (r *BindClusterReconciler) reconcileStatefulSet(ctx context.Context, c *bin
Name: bind.ContainerName,
Image: image,
ImagePullPolicy: c.Spec.ImagePullPolicy,
Command: []string{"/bin/sh", "/etc/bind/entrypoint.sh"},
Command: []string{"/bin/sh", bind.EntrypointPath},
Ports: []corev1.ContainerPort{
{Name: "dns-udp", ContainerPort: 53, Protocol: corev1.ProtocolUDP},
{Name: "dns-tcp", ContainerPort: 53, Protocol: corev1.ProtocolTCP},
},
Resources: c.Spec.Resources,
VolumeMounts: []corev1.VolumeMount{
{Name: "bind-etc", MountPath: "/etc/bind", ReadOnly: true},
{Name: "run", MountPath: "/run/named"},
{Name: "bind-etc", MountPath: bind.ConfigDir, ReadOnly: true},
{Name: "run", MountPath: bind.RunDir},
{Name: "data", MountPath: bind.DataDir},
},
ReadinessProbe: &corev1.Probe{
+3
View File
@@ -23,6 +23,9 @@ const (
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" }
+8 -6
View File
@@ -2,6 +2,7 @@ package controller
import (
"context"
"fmt"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -10,6 +11,7 @@ import (
ctrl "sigs.k8s.io/controller-runtime"
bindv1alpha1 "git.unkin.net/unkin/bind-operator/api/v1alpha1"
"git.unkin.net/unkin/bind-operator/internal/bind"
)
func intstrFromInt(i int) intstr.IntOrString { return intstr.FromInt(i) }
@@ -26,17 +28,17 @@ func podReady(pod *corev1.Pod) bool {
// entrypointScript selects the primary or secondary named.conf based on the
// pod's StatefulSet ordinal and launches named in the foreground.
func entrypointScript() string {
return `#!/bin/sh
return fmt.Sprintf(`#!/bin/sh
set -eu
ORD="${HOSTNAME##*-}"
if [ "$ORD" = "0" ]; then
cp /etc/bind/named.conf.primary /run/named/named.conf
cp %[1]s %[3]s
else
cp /etc/bind/named.conf.secondary /run/named/named.conf
cp %[2]s %[3]s
fi
mkdir -p /var/lib/named/zones /var/lib/named/catalog
exec named -g -c /run/named/named.conf
`
mkdir -p %[4]s/zones %[4]s/catalog
exec %[5]s -g -c %[3]s
`, bind.NamedConfPrimary, bind.NamedConfSecondary, bind.NamedConfRun, bind.DataDir, bind.NamedBin)
}
func (r *BindClusterReconciler) upsertService(ctx context.Context, c *bindv1alpha1.BindCluster, desired *corev1.Service) error {