diff --git a/README.md b/README.md index 8fc5d7c..9cd5155 100644 --- a/README.md +++ b/README.md @@ -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; diff --git a/api/v1alpha1/bindcluster_types.go b/api/v1alpha1/bindcluster_types.go index b747ede..e6fc166 100644 --- a/api/v1alpha1/bindcluster_types.go +++ b/api/v1alpha1/bindcluster_types.go @@ -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"` diff --git a/config/crd/bases/bind.unkin.net_bindclusters.yaml b/config/crd/bases/bind.unkin.net_bindclusters.yaml index dad7eb4..4485e70 100644 --- a/config/crd/bases/bind.unkin.net_bindclusters.yaml +++ b/config/crd/bases/bind.unkin.net_bindclusters.yaml @@ -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. diff --git a/internal/bind/consts.go b/internal/bind/consts.go new file mode 100644 index 0000000..7f2e383 --- /dev/null +++ b/internal/bind/consts.go @@ -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" +) diff --git a/internal/bind/exec.go b/internal/bind/exec.go index 3b6117e..baddbb4 100644 --- a/internal/bind/exec.go +++ b/internal/bind/exec.go @@ -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 diff --git a/internal/bind/nsupdate.go b/internal/bind/nsupdate.go index 22f8769..5fd4795 100644 --- a/internal/bind/nsupdate.go +++ b/internal/bind/nsupdate.go @@ -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) } diff --git a/internal/bind/render.go b/internal/bind/render.go index 89d60fa..86ccc29 100644 --- a/internal/bind/render.go +++ b/internal/bind/render.go @@ -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...) diff --git a/internal/bind/rndc.go b/internal/bind/rndc.go index 35f6336..ba05f04 100644 --- a/internal/bind/rndc.go +++ b/internal/bind/rndc.go @@ -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 ` 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...), "") } diff --git a/internal/controller/bindcluster_controller.go b/internal/controller/bindcluster_controller.go index c9b687e..4aae406 100644 --- a/internal/controller/bindcluster_controller.go +++ b/internal/controller/bindcluster_controller.go @@ -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{ diff --git a/internal/controller/helpers.go b/internal/controller/helpers.go index 33a2758..051cf12 100644 --- a/internal/controller/helpers.go +++ b/internal/controller/helpers.go @@ -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" } diff --git a/internal/controller/util.go b/internal/controller/util.go index 5a88994..e2b6905 100644 --- a/internal/controller/util.go +++ b/internal/controller/util.go @@ -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 {