Initial bind-operator: 9 CRDs + controllers

Implements a Kubernetes operator that manages fleets of BIND9 servers
declaratively, using controller-runtime (matching forgebot conventions).

- add BindCluster reconciler: StatefulSet (pod-0 primary, secondaries),
  headless + client Services, rendered named.conf ConfigMap, TSIG keys
  Secret and rndc control Secret; watches dependent CRs to re-render
- add BindTSIGKey reconciler that generates key material into a Secret
- add BindZone/DNSRecord reconcilers using fully-dynamic delivery
  (rndc addzone + TSIG nsupdate against the primary pod)
- add BindCatalogZone reconciler so secondaries auto-provision zones
- add BindPolicy (RPZ), BindDNSSECPolicy, BindView, BindACL reconcilers
- render primary/secondary named.conf variants selected by pod ordinal
- generate CRDs, deepcopy and RBAC; add samples mapping the three Puppet
  roles (authoritative/resolver/external-dns) to three BindClusters
- add Makefile, Dockerfile.operator, Woodpecker CI and kind manifests
This commit is contained in:
2026-07-03 15:48:13 +10:00
parent b3a5b4d0b7
commit fe5fbdaf6d
63 changed files with 8240 additions and 1 deletions
+61
View File
@@ -0,0 +1,61 @@
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// BindACLSpec defines a reusable named address_match_list.
type BindACLSpec struct {
// ClusterRef names the BindCluster whose named.conf this ACL is rendered
// into. When empty the ACL is available to every cluster in the namespace.
// +optional
ClusterRef string `json:"clusterRef,omitempty"`
// Entries are raw BIND address-match-list elements, e.g. "10.0.0.0/8",
// "!192.168.1.5", "key transfer-key", "localhost", "any", or the name of
// another ACL.
// +kubebuilder:validation:MinItems=1
Entries []string `json:"entries"`
}
// BindACLStatus reports observed ACL state.
type BindACLStatus struct {
// +optional
Ready bool `json:"ready,omitempty"`
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// +optional
// +listType=map
// +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:shortName=bacl
// +kubebuilder:printcolumn:name="Cluster",type=string,JSONPath=`.spec.clusterRef`
// +kubebuilder:printcolumn:name="Entries",type=integer,JSONPath=`.spec.entries[*]`
// +kubebuilder:printcolumn:name="Ready",type=boolean,JSONPath=`.status.ready`
// BindACL is a named address-match-list referenced by views, zones and
// policies for match-clients / allow-query / allow-transfer / allow-update.
type BindACL struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec BindACLSpec `json:"spec,omitempty"`
Status BindACLStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// BindACLList contains a list of BindACL.
type BindACLList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []BindACL `json:"items"`
}
func init() {
SchemeBuilder.Register(&BindACL{}, &BindACLList{})
}
+71
View File
@@ -0,0 +1,71 @@
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// BindCatalogZoneSpec defines a BIND9 catalog zone. The primary publishes it
// and secondaries consume it, so member zones are provisioned onto every
// secondary automatically without per-zone reconfiguration.
type BindCatalogZoneSpec struct {
// ClusterRef names the owning BindCluster.
ClusterRef string `json:"clusterRef"`
// ZoneName is the catalog zone's own origin, e.g. "catalog.internal".
ZoneName string `json:"zoneName"`
// DefaultPrimaries are the addresses member zones point at on secondaries.
// Defaults to the cluster primary Service.
// +optional
DefaultPrimaries []string `json:"defaultPrimaries,omitempty"`
// TransferKeyRef names the BindTSIGKey authenticating catalog + member zone
// transfers to secondaries.
// +optional
TransferKeyRef string `json:"transferKeyRef,omitempty"`
}
// BindCatalogZoneStatus reports observed catalog state.
type BindCatalogZoneStatus struct {
// +optional
Ready bool `json:"ready,omitempty"`
// MemberCount is the number of member zones registered in the catalog.
// +optional
MemberCount int32 `json:"memberCount,omitempty"`
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// +optional
// +listType=map
// +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:shortName=bcz
// +kubebuilder:printcolumn:name="Cluster",type=string,JSONPath=`.spec.clusterRef`
// +kubebuilder:printcolumn:name="Zone",type=string,JSONPath=`.spec.zoneName`
// +kubebuilder:printcolumn:name="Members",type=integer,JSONPath=`.status.memberCount`
// +kubebuilder:printcolumn:name="Ready",type=boolean,JSONPath=`.status.ready`
// BindCatalogZone auto-provisions member zones onto cluster secondaries.
type BindCatalogZone struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec BindCatalogZoneSpec `json:"spec,omitempty"`
Status BindCatalogZoneStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// BindCatalogZoneList contains a list of BindCatalogZone.
type BindCatalogZoneList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []BindCatalogZone `json:"items"`
}
func init() {
SchemeBuilder.Register(&BindCatalogZone{}, &BindCatalogZoneList{})
}
+181
View File
@@ -0,0 +1,181 @@
package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// BindMode selects the behaviour of a BindCluster and maps onto a classic
// BIND deployment role.
// - authoritative: serves signed/unsigned authoritative zones. Ordinal-0 is
// the primary that holds zone data; the remaining pods are secondaries that
// replicate via AXFR/IXFR + NOTIFY (optionally driven by a catalog zone).
// - resolver: N identical recursive resolvers, no zone replication.
// - dynamic: like authoritative, but the primary accepts RFC2136 TSIG updates
// (the external-dns pattern); secondaries replicate the result.
//
// +kubebuilder:validation:Enum=authoritative;resolver;dynamic
type BindMode string
const (
ModeAuthoritative BindMode = "authoritative"
ModeResolver BindMode = "resolver"
ModeDynamic BindMode = "dynamic"
)
// ClusterServiceSpec controls how the cluster is exposed to clients.
type ClusterServiceSpec struct {
// Type of the client-facing Service. Defaults to ClusterIP.
// +kubebuilder:validation:Enum=ClusterIP;LoadBalancer;NodePort
// +optional
Type corev1.ServiceType `json:"type,omitempty"`
// LoadBalancerIP requests a specific address when Type is LoadBalancer.
// +optional
LoadBalancerIP string `json:"loadBalancerIP,omitempty"`
// Annotations added to the client-facing Service (e.g. PureLB/MetalLB hints).
// +optional
Annotations map[string]string `json:"annotations,omitempty"`
}
// BindClusterSpec defines the desired state of a BIND cluster.
type BindClusterSpec struct {
// Mode selects the cluster role.
// +kubebuilder:default=authoritative
Mode BindMode `json:"mode"`
// Replicas is the number of BIND pods. Ordinal-0 is the primary for the
// authoritative and dynamic modes.
// +kubebuilder:default=3
// +kubebuilder:validation:Minimum=1
// +optional
Replicas int32 `json:"replicas,omitempty"`
// Image is the BIND9 container image.
// +kubebuilder:default="git.unkin.net/unkin/bind9:latest"
// +optional
Image string `json:"image,omitempty"`
// ImagePullPolicy for the BIND container.
// +optional
ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"`
// Recursion overrides the default per-mode recursion setting. When nil,
// resolver mode enables recursion and the other modes disable it.
// +optional
Recursion *bool `json:"recursion,omitempty"`
// Forwarders is a list of upstream resolvers used by resolver mode (and any
// forward zones that do not specify their own).
// +optional
Forwarders []string `json:"forwarders,omitempty"`
// AllowNewZones enables the rndc addzone/delzone control path required for
// dynamic zone provisioning. Defaults to true.
// +kubebuilder:default=true
// +optional
AllowNewZones *bool `json:"allowNewZones,omitempty"`
// CatalogZoneRef names a BindCatalogZone that secondaries consume so member
// zones are auto-provisioned without per-zone reconfiguration.
// +optional
CatalogZoneRef string `json:"catalogZoneRef,omitempty"`
// ExtraOptions are raw named.conf `options { ... }` lines appended verbatim.
// +optional
ExtraOptions []string `json:"extraOptions,omitempty"`
// StorageClassName for the per-pod PVC that holds zone data and journals.
// +optional
StorageClassName *string `json:"storageClassName,omitempty"`
// StorageSize for the per-pod PVC. Defaults to 1Gi.
// +kubebuilder:default="1Gi"
// +optional
StorageSize string `json:"storageSize,omitempty"`
// Resources for the BIND container.
// +optional
Resources corev1.ResourceRequirements `json:"resources,omitempty"`
// Service controls how the cluster is exposed.
// +optional
Service ClusterServiceSpec `json:"service,omitempty"`
// NodeSelector for the BIND pods.
// +optional
NodeSelector map[string]string `json:"nodeSelector,omitempty"`
// Tolerations for the BIND pods.
// +optional
Tolerations []corev1.Toleration `json:"tolerations,omitempty"`
// Affinity for the BIND pods.
// +optional
Affinity *corev1.Affinity `json:"affinity,omitempty"`
}
// BindClusterStatus reports observed cluster state.
type BindClusterStatus struct {
// Phase is a coarse lifecycle summary.
// +optional
Phase string `json:"phase,omitempty"`
// Replicas is the number of BIND pods requested.
// +optional
Replicas int32 `json:"replicas,omitempty"`
// ReadyReplicas is the number of BIND pods currently ready.
// +optional
ReadyReplicas int32 `json:"readyReplicas,omitempty"`
// PrimaryPod is the pod that holds authoritative zone data (ordinal-0).
// +optional
PrimaryPod string `json:"primaryPod,omitempty"`
// PrimaryService is the in-cluster DNS name secondaries transfer from.
// +optional
PrimaryService string `json:"primaryService,omitempty"`
// ObservedGeneration is the last reconciled generation.
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// Conditions represent the latest available observations.
// +optional
// +listType=map
// +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:shortName=bc
// +kubebuilder:printcolumn:name="Mode",type=string,JSONPath=`.spec.mode`
// +kubebuilder:printcolumn:name="Desired",type=integer,JSONPath=`.spec.replicas`
// +kubebuilder:printcolumn:name="Ready",type=integer,JSONPath=`.status.readyReplicas`
// +kubebuilder:printcolumn:name="Primary",type=string,JSONPath=`.status.primaryPod`
// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase`
// BindCluster is a managed set of BIND9 servers.
type BindCluster struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec BindClusterSpec `json:"spec,omitempty"`
Status BindClusterStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// BindClusterList contains a list of BindCluster.
type BindClusterList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []BindCluster `json:"items"`
}
func init() {
SchemeBuilder.Register(&BindCluster{}, &BindClusterList{})
}
+111
View File
@@ -0,0 +1,111 @@
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// DNSSECKey describes a key in a signing policy.
type DNSSECKey struct {
// Lifetime is how long the key is used before rollover, e.g. "P30D" or
// "unlimited". Empty means unlimited.
// +optional
Lifetime string `json:"lifetime,omitempty"`
// Algorithm overrides the policy algorithm for this key.
// +optional
Algorithm string `json:"algorithm,omitempty"`
// KeySize in bits for RSA algorithms (ignored for ECDSA/EdDSA).
// +optional
KeySize int32 `json:"keySize,omitempty"`
}
// BindDNSSECPolicySpec mirrors a BIND9 dnssec-policy. Zones referencing it are
// signed with inline-signing and automated key management.
type BindDNSSECPolicySpec struct {
// ClusterRef names the owning BindCluster.
ClusterRef string `json:"clusterRef"`
// PolicyName is the dnssec-policy name in named.conf. Defaults to the object
// name.
// +optional
PolicyName string `json:"policyName,omitempty"`
// Algorithm for signing. Defaults to ecdsap256sha256.
// +kubebuilder:default="ecdsap256sha256"
// +optional
Algorithm string `json:"algorithm,omitempty"`
// CSK, when set, uses a Combined Signing Key instead of split KSK/ZSK.
// +optional
CSK *DNSSECKey `json:"csk,omitempty"`
// KSK is the Key Signing Key configuration (ignored when CSK is set).
// +optional
KSK *DNSSECKey `json:"ksk,omitempty"`
// ZSK is the Zone Signing Key configuration (ignored when CSK is set).
// +optional
ZSK *DNSSECKey `json:"zsk,omitempty"`
// NSEC3 enables NSEC3 hashing instead of NSEC.
// +optional
NSEC3 bool `json:"nsec3,omitempty"`
// MaxZoneTTL, e.g. "P1D".
// +optional
MaxZoneTTL string `json:"maxZoneTTL,omitempty"`
// SignaturesValidity, e.g. "P14D".
// +optional
SignaturesValidity string `json:"signaturesValidity,omitempty"`
// ExtraOptions are raw named.conf lines appended inside the policy block.
// +optional
ExtraOptions []string `json:"extraOptions,omitempty"`
}
// BindDNSSECPolicyStatus reports observed policy state.
type BindDNSSECPolicyStatus struct {
// +optional
Ready bool `json:"ready,omitempty"`
// ZoneCount is the number of zones signed with this policy.
// +optional
ZoneCount int32 `json:"zoneCount,omitempty"`
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// +optional
// +listType=map
// +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:shortName=bdp
// +kubebuilder:printcolumn:name="Cluster",type=string,JSONPath=`.spec.clusterRef`
// +kubebuilder:printcolumn:name="Algorithm",type=string,JSONPath=`.spec.algorithm`
// +kubebuilder:printcolumn:name="Zones",type=integer,JSONPath=`.status.zoneCount`
// +kubebuilder:printcolumn:name="Ready",type=boolean,JSONPath=`.status.ready`
// BindDNSSECPolicy is a reusable DNSSEC signing policy.
type BindDNSSECPolicy struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec BindDNSSECPolicySpec `json:"spec,omitempty"`
Status BindDNSSECPolicyStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// BindDNSSECPolicyList contains a list of BindDNSSECPolicy.
type BindDNSSECPolicyList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []BindDNSSECPolicy `json:"items"`
}
func init() {
SchemeBuilder.Register(&BindDNSSECPolicy{}, &BindDNSSECPolicyList{})
}
+110
View File
@@ -0,0 +1,110 @@
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// RPZTrigger is the match domain of a response-policy rule.
// +kubebuilder:validation:Enum=qname;client-ip;ip;nsdname;nsip
type RPZTrigger string
// RPZAction is the policy action taken on a match.
// +kubebuilder:validation:Enum=nxdomain;nodata;passthru;drop;tcp-only;cname
type RPZAction string
// RPZRule is a single response-policy rule.
type RPZRule struct {
// Trigger selects what the Match is compared against.
// +kubebuilder:default=qname
// +optional
Trigger RPZTrigger `json:"trigger,omitempty"`
// Match is the trigger value, e.g. a domain "bad.example." or CIDR.
Match string `json:"match"`
// Action taken when the rule matches.
// +kubebuilder:default=nxdomain
// +optional
Action RPZAction `json:"action,omitempty"`
// Target is the rewrite target when Action is cname.
// +optional
Target string `json:"target,omitempty"`
}
// BindPolicySpec defines a Response Policy Zone (RPZ) — a DNS firewall applied
// to a resolver cluster.
type BindPolicySpec struct {
// ClusterRef names the owning BindCluster (typically a resolver).
ClusterRef string `json:"clusterRef"`
// ViewRef optionally scopes the policy to a single view.
// +optional
ViewRef string `json:"viewRef,omitempty"`
// ZoneName is the RPZ zone origin, e.g. "rpz.internal".
ZoneName string `json:"zoneName"`
// Order controls this policy's position in the response-policy clause.
// +kubebuilder:default=100
// +optional
Order int32 `json:"order,omitempty"`
// Rules are the inline policy triggers.
// +optional
Rules []RPZRule `json:"rules,omitempty"`
// Primaries lets the RPZ zone be transferred from an external feed instead
// of being locally populated.
// +optional
Primaries []string `json:"primaries,omitempty"`
// TransferKeyRef names the BindTSIGKey used to pull from Primaries.
// +optional
TransferKeyRef string `json:"transferKeyRef,omitempty"`
}
// BindPolicyStatus reports observed policy state.
type BindPolicyStatus struct {
// +optional
Ready bool `json:"ready,omitempty"`
// RuleCount is the number of active rules.
// +optional
RuleCount int32 `json:"ruleCount,omitempty"`
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// +optional
// +listType=map
// +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:shortName=bp
// +kubebuilder:printcolumn:name="Cluster",type=string,JSONPath=`.spec.clusterRef`
// +kubebuilder:printcolumn:name="Zone",type=string,JSONPath=`.spec.zoneName`
// +kubebuilder:printcolumn:name="Rules",type=integer,JSONPath=`.status.ruleCount`
// +kubebuilder:printcolumn:name="Ready",type=boolean,JSONPath=`.status.ready`
// BindPolicy is a Response Policy Zone (RPZ) applied to a cluster.
type BindPolicy struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec BindPolicySpec `json:"spec,omitempty"`
Status BindPolicyStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// BindPolicyList contains a list of BindPolicy.
type BindPolicyList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []BindPolicy `json:"items"`
}
func init() {
SchemeBuilder.Register(&BindPolicy{}, &BindPolicyList{})
}
+92
View File
@@ -0,0 +1,92 @@
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// TSIGAlgorithm is a supported TSIG HMAC algorithm.
// +kubebuilder:validation:Enum=hmac-sha256;hmac-sha512;hmac-sha384;hmac-sha224;hmac-sha1;hmac-md5
type TSIGAlgorithm string
const (
TSIGHMACSHA256 TSIGAlgorithm = "hmac-sha256"
TSIGHMACSHA512 TSIGAlgorithm = "hmac-sha512"
)
// BindTSIGKeySpec defines a TSIG key. If no existing key material is imported,
// the operator generates a random key and stores it in a Secret.
type BindTSIGKeySpec struct {
// Algorithm is the HMAC algorithm. Defaults to hmac-sha256.
// +kubebuilder:default="hmac-sha256"
// +optional
Algorithm TSIGAlgorithm `json:"algorithm,omitempty"`
// KeyName is the TSIG key name emitted into named.conf. Defaults to the
// object name.
// +optional
KeyName string `json:"keyName,omitempty"`
// SecretName is the Secret the key material is written to (or read from when
// ImportExisting is set). Defaults to "<name>-tsig".
// +optional
SecretName string `json:"secretName,omitempty"`
// ImportExisting, when true, means the referenced Secret already contains a
// `secret` key and the operator will not generate new material.
// +optional
ImportExisting bool `json:"importExisting,omitempty"`
}
// BindTSIGKeyStatus reports observed TSIG key state.
type BindTSIGKeyStatus struct {
// SecretName holds the generated/managed key material.
// +optional
SecretName string `json:"secretName,omitempty"`
// KeyName as used in named.conf.
// +optional
KeyName string `json:"keyName,omitempty"`
// Ready is true once the key Secret exists.
// +optional
Ready bool `json:"ready,omitempty"`
// ObservedGeneration is the last reconciled generation.
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// +optional
// +listType=map
// +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:shortName=btk
// +kubebuilder:printcolumn:name="Algorithm",type=string,JSONPath=`.spec.algorithm`
// +kubebuilder:printcolumn:name="Secret",type=string,JSONPath=`.status.secretName`
// +kubebuilder:printcolumn:name="Ready",type=boolean,JSONPath=`.status.ready`
// BindTSIGKey is a TSIG key backing zone transfers, dynamic updates and view
// matching. The key material lives in a Kubernetes Secret, never in the CR.
type BindTSIGKey struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec BindTSIGKeySpec `json:"spec,omitempty"`
Status BindTSIGKeyStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// BindTSIGKeyList contains a list of BindTSIGKey.
type BindTSIGKeyList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []BindTSIGKey `json:"items"`
}
func init() {
SchemeBuilder.Register(&BindTSIGKey{}, &BindTSIGKeyList{})
}
+84
View File
@@ -0,0 +1,84 @@
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// BindViewSpec defines a split-horizon view. View ordering is significant in
// BIND; use Order to control the sequence in named.conf.
type BindViewSpec struct {
// ClusterRef names the owning BindCluster.
ClusterRef string `json:"clusterRef"`
// Order controls the position of this view in named.conf (ascending). The
// first view whose match-clients matches a query wins.
// +kubebuilder:default=100
// +optional
Order int32 `json:"order,omitempty"`
// MatchClients is an address-match-list (inline entries and/or ACL names)
// selecting which clients this view answers. Defaults to "any".
// +optional
MatchClients []string `json:"matchClients,omitempty"`
// MatchDestinations is an optional destination address-match-list.
// +optional
MatchDestinations []string `json:"matchDestinations,omitempty"`
// Recursion overrides the cluster recursion setting for this view.
// +optional
Recursion *bool `json:"recursion,omitempty"`
// AllowQuery is an address-match-list restricting queries into this view.
// +optional
AllowQuery []string `json:"allowQuery,omitempty"`
// ExtraOptions are raw named.conf lines appended inside the view block.
// +optional
ExtraOptions []string `json:"extraOptions,omitempty"`
}
// BindViewStatus reports observed view state.
type BindViewStatus struct {
// +optional
Ready bool `json:"ready,omitempty"`
// ZoneCount is the number of zones currently bound to this view.
// +optional
ZoneCount int32 `json:"zoneCount,omitempty"`
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// +optional
// +listType=map
// +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:shortName=bv
// +kubebuilder:printcolumn:name="Cluster",type=string,JSONPath=`.spec.clusterRef`
// +kubebuilder:printcolumn:name="Order",type=integer,JSONPath=`.spec.order`
// +kubebuilder:printcolumn:name="Zones",type=integer,JSONPath=`.status.zoneCount`
// +kubebuilder:printcolumn:name="Ready",type=boolean,JSONPath=`.status.ready`
// BindView is a split-horizon view on a BindCluster.
type BindView struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec BindViewSpec `json:"spec,omitempty"`
Status BindViewStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// BindViewList contains a list of BindView.
type BindViewList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []BindView `json:"items"`
}
func init() {
SchemeBuilder.Register(&BindView{}, &BindViewList{})
}
+156
View File
@@ -0,0 +1,156 @@
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// ZoneType is the BIND zone type.
// +kubebuilder:validation:Enum=primary;secondary;forward;stub
type ZoneType string
const (
ZonePrimary ZoneType = "primary"
ZoneSecondary ZoneType = "secondary"
ZoneForward ZoneType = "forward"
ZoneStub ZoneType = "stub"
)
// Record is a single resource record set seeded into a primary zone via
// dynamic update (nsupdate). Ongoing changes may also arrive from DNSRecord
// objects or external RFC2136 clients.
type Record struct {
// Name is the owner name, relative to the zone apex or fully qualified.
// Use "@" for the apex.
// +kubebuilder:default="@"
// +optional
Name string `json:"name,omitempty"`
// Type is the RR type, e.g. A, AAAA, CNAME, MX, TXT, SRV, NS, PTR, CAA.
Type string `json:"type"`
// TTL for the record set in seconds. Falls back to the zone default TTL.
// +optional
TTL *int32 `json:"ttl,omitempty"`
// Values are the RDATA entries, e.g. ["10 mail.example.com."] for an MX or
// ["192.0.2.1","192.0.2.2"] for an A round-robin.
// +kubebuilder:validation:MinItems=1
Values []string `json:"values"`
}
// BindZoneSpec defines a DNS zone managed on a BindCluster's primary.
type BindZoneSpec struct {
// ClusterRef names the owning BindCluster.
ClusterRef string `json:"clusterRef"`
// ViewRef optionally binds this zone to a BindView.
// +optional
ViewRef string `json:"viewRef,omitempty"`
// ZoneName is the DNS origin, e.g. "example.com" or "2.0.192.in-addr.arpa".
ZoneName string `json:"zoneName"`
// Type is the zone type. Defaults to primary.
// +kubebuilder:default=primary
// +optional
Type ZoneType `json:"type,omitempty"`
// DefaultTTL for records that do not set their own TTL. Defaults to 3600.
// +kubebuilder:default=3600
// +optional
DefaultTTL int32 `json:"defaultTTL,omitempty"`
// Records are static record sets seeded into a primary zone.
// +optional
Records []Record `json:"records,omitempty"`
// DynamicUpdate enables RFC2136 updates for this zone (external-dns style).
// When true, UpdateKeyRef must reference a BindTSIGKey.
// +optional
DynamicUpdate bool `json:"dynamicUpdate,omitempty"`
// UpdateKeyRef names the BindTSIGKey permitted to send dynamic updates.
// +optional
UpdateKeyRef string `json:"updateKeyRef,omitempty"`
// AllowTransfer is an address-match-list (inline entries and/or ACL/key
// names) permitted to AXFR/IXFR this zone.
// +optional
AllowTransfer []string `json:"allowTransfer,omitempty"`
// Catalog, when true, registers this zone as a member of the cluster's
// catalog zone so secondaries auto-provision it.
// +kubebuilder:default=true
// +optional
Catalog *bool `json:"catalog,omitempty"`
// DNSSECPolicyRef names a BindDNSSECPolicy to sign this zone with.
// +optional
DNSSECPolicyRef string `json:"dnssecPolicyRef,omitempty"`
// Forwarders lists upstreams for a forward-type zone.
// +optional
Forwarders []string `json:"forwarders,omitempty"`
// Primaries lists source servers for a secondary/stub-type zone.
// +optional
Primaries []string `json:"primaries,omitempty"`
// TransferKeyRef names the BindTSIGKey used to authenticate transfers from
// Primaries for a secondary zone.
// +optional
TransferKeyRef string `json:"transferKeyRef,omitempty"`
}
// BindZoneStatus reports observed zone state.
type BindZoneStatus struct {
// Phase is a coarse lifecycle summary (Pending/Ready/Error).
// +optional
Phase string `json:"phase,omitempty"`
// Serial is the last observed SOA serial on the primary.
// +optional
Serial int64 `json:"serial,omitempty"`
// RecordCount is the number of managed record sets applied.
// +optional
RecordCount int32 `json:"recordCount,omitempty"`
// Signed reports whether DNSSEC signing is active.
// +optional
Signed bool `json:"signed,omitempty"`
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// +optional
// +listType=map
// +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:shortName=bz
// +kubebuilder:printcolumn:name="Zone",type=string,JSONPath=`.spec.zoneName`
// +kubebuilder:printcolumn:name="Type",type=string,JSONPath=`.spec.type`
// +kubebuilder:printcolumn:name="Cluster",type=string,JSONPath=`.spec.clusterRef`
// +kubebuilder:printcolumn:name="Serial",type=integer,JSONPath=`.status.serial`
// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase`
// BindZone is a forward or reverse DNS zone.
type BindZone struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec BindZoneSpec `json:"spec,omitempty"`
Status BindZoneStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// BindZoneList contains a list of BindZone.
type BindZoneList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []BindZone `json:"items"`
}
func init() {
SchemeBuilder.Register(&BindZone{}, &BindZoneList{})
}
+75
View File
@@ -0,0 +1,75 @@
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// DNSRecordSpec defines a single record set applied to a zone via TSIG dynamic
// update (nsupdate) — the external-dns write path expressed as a CRD.
type DNSRecordSpec struct {
// ZoneRef names the BindZone this record belongs to. The cluster, view and
// update key are derived from the referenced zone.
ZoneRef string `json:"zoneRef"`
// Name is the owner name, relative to the zone apex or fully qualified.
// +kubebuilder:default="@"
// +optional
Name string `json:"name,omitempty"`
// Type is the RR type, e.g. A, AAAA, CNAME, TXT, SRV, MX.
Type string `json:"type"`
// TTL for the record set in seconds. Falls back to the zone default TTL.
// +optional
TTL *int32 `json:"ttl,omitempty"`
// Values are the RDATA entries.
// +kubebuilder:validation:MinItems=1
Values []string `json:"values"`
}
// DNSRecordStatus reports observed record state.
type DNSRecordStatus struct {
// Phase is a coarse lifecycle summary (Pending/Applied/Error).
// +optional
Phase string `json:"phase,omitempty"`
// FQDN is the fully-qualified owner name that was applied.
// +optional
FQDN string `json:"fqdn,omitempty"`
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// +optional
// +listType=map
// +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:shortName=dnsr
// +kubebuilder:printcolumn:name="Zone",type=string,JSONPath=`.spec.zoneRef`
// +kubebuilder:printcolumn:name="Name",type=string,JSONPath=`.spec.name`
// +kubebuilder:printcolumn:name="Type",type=string,JSONPath=`.spec.type`
// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase`
// DNSRecord is an individually-managed record set applied to a BindZone.
type DNSRecord struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec DNSRecordSpec `json:"spec,omitempty"`
Status DNSRecordStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// DNSRecordList contains a list of DNSRecord.
type DNSRecordList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []DNSRecord `json:"items"`
}
func init() {
SchemeBuilder.Register(&DNSRecord{}, &DNSRecordList{})
}
+3
View File
@@ -0,0 +1,3 @@
// +kubebuilder:object:generate=true
// +groupName=bind.unkin.net
package v1alpha1
+17
View File
@@ -0,0 +1,17 @@
package v1alpha1
import (
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/scheme"
)
var (
// GroupVersion is the group/version used to register these objects.
GroupVersion = schema.GroupVersion{Group: "bind.unkin.net", Version: "v1alpha1"}
// SchemeBuilder registers the API types with a runtime scheme.
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
// AddToScheme adds the types in this group-version to the given scheme.
AddToScheme = SchemeBuilder.AddToScheme
)
File diff suppressed because it is too large Load Diff