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:
@@ -0,0 +1,4 @@
|
|||||||
|
/bin/
|
||||||
|
*.out
|
||||||
|
*.test
|
||||||
|
.env
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
when:
|
||||||
|
- event: pull_request
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: docker-build-operator
|
||||||
|
image: woodpeckerci/plugin-docker-buildx
|
||||||
|
settings:
|
||||||
|
repo: git.unkin.net/unkin/bind-operator
|
||||||
|
dockerfile: Dockerfile.operator
|
||||||
|
dry_run: true
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
when:
|
||||||
|
- event: tag
|
||||||
|
ref: refs/tags/v*
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: docker-operator
|
||||||
|
image: woodpeckerci/plugin-docker-buildx
|
||||||
|
settings:
|
||||||
|
registry: git.unkin.net
|
||||||
|
repo: git.unkin.net/unkin/bind-operator
|
||||||
|
dockerfile: Dockerfile.operator
|
||||||
|
username: droneci
|
||||||
|
password:
|
||||||
|
from_secret: DRONECI_PASSWORD
|
||||||
|
tags:
|
||||||
|
- ${CI_COMMIT_TAG}
|
||||||
|
- latest
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
when:
|
||||||
|
- event: pull_request
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: pre-commit
|
||||||
|
image: golang:1.25
|
||||||
|
commands:
|
||||||
|
- test -z "$(gofmt -l .)"
|
||||||
|
- go vet ./...
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
when:
|
||||||
|
- event: pull_request
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: test
|
||||||
|
image: golang:1.25
|
||||||
|
commands:
|
||||||
|
- go test -race -count=1 ./api/... ./internal/...
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
FROM golang:1.25-alpine AS builder
|
||||||
|
|
||||||
|
RUN apk add --no-cache git
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o bind-operator ./cmd/operator
|
||||||
|
|
||||||
|
FROM gcr.io/distroless/static-debian12:nonroot
|
||||||
|
|
||||||
|
COPY --from=builder /build/bind-operator /usr/local/bin/bind-operator
|
||||||
|
|
||||||
|
ENTRYPOINT ["bind-operator"]
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
.PHONY: build test lint fmt generate manifests docker-operator clean tidy patch minor major
|
||||||
|
|
||||||
|
BINARY_OP := bin/bind-operator
|
||||||
|
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "0.0.0-dev")
|
||||||
|
|
||||||
|
build: tidy
|
||||||
|
go build -ldflags="-s -w" -o $(BINARY_OP) ./cmd/operator
|
||||||
|
|
||||||
|
test:
|
||||||
|
go test -race -count=1 ./api/... ./internal/...
|
||||||
|
|
||||||
|
lint:
|
||||||
|
go vet ./...
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
gofmt -w .
|
||||||
|
|
||||||
|
## generate: regenerate deepcopy, CRDs and RBAC from kubebuilder markers
|
||||||
|
generate:
|
||||||
|
controller-gen object paths="./api/..."
|
||||||
|
controller-gen crd paths="./api/..." output:crd:artifacts:config=config/crd/bases
|
||||||
|
controller-gen rbac:roleName=bind-operator paths="./internal/controller/..." output:rbac:dir=config/rbac
|
||||||
|
|
||||||
|
manifests: generate
|
||||||
|
|
||||||
|
docker-operator:
|
||||||
|
docker build -t bind-operator:$(VERSION) -f Dockerfile.operator .
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf bin/
|
||||||
|
|
||||||
|
tidy:
|
||||||
|
go mod tidy
|
||||||
|
|
||||||
|
_LATEST := $(shell git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$$' | head -1)
|
||||||
|
_BASE := $(if $(_LATEST),$(_LATEST),v0.0.0)
|
||||||
|
_MAJ := $(shell echo $(_BASE) | sed 's/^v//' | cut -d. -f1)
|
||||||
|
_MIN := $(shell echo $(_BASE) | sed 's/^v//' | cut -d. -f2)
|
||||||
|
_PAT := $(shell echo $(_BASE) | sed 's/^v//' | cut -d. -f3)
|
||||||
|
|
||||||
|
patch:
|
||||||
|
@NEW=v$(_MAJ).$(_MIN).$(shell expr $(_PAT) + 1); \
|
||||||
|
git tag $$NEW && echo "Tagged $$NEW" && git push origin $$NEW
|
||||||
|
|
||||||
|
minor:
|
||||||
|
@NEW=v$(_MAJ).$(shell expr $(_MIN) + 1).0; \
|
||||||
|
git tag $$NEW && echo "Tagged $$NEW" && git push origin $$NEW
|
||||||
|
|
||||||
|
major:
|
||||||
|
@NEW=v$(shell expr $(_MAJ) + 1).0.0; \
|
||||||
|
git tag $$NEW && echo "Tagged $$NEW" && git push origin $$NEW
|
||||||
@@ -1,3 +1,98 @@
|
|||||||
# bind-operator
|
# bind-operator
|
||||||
|
|
||||||
Kubernetes operator for managing BIND9 DNS clusters, zones, views, and TSIG keys
|
A Kubernetes operator that manages fleets of BIND9 servers declaratively:
|
||||||
|
StatefulSet-backed clusters with primary/secondary replication, and zones,
|
||||||
|
views, TSIG keys, ACLs, catalog zones, RPZ policies and DNSSEC policies as
|
||||||
|
custom resources.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
Each `BindCluster` is a StatefulSet plus a headless Service (stable per-pod DNS)
|
||||||
|
and a client Service. **Ordinal-0 is the primary**; the remaining pods are
|
||||||
|
secondaries that replicate via AXFR/IXFR + NOTIFY. A per-pod PVC holds zone
|
||||||
|
databases and journals.
|
||||||
|
|
||||||
|
Zone content is delivered **dynamically**: the operator execs `rndc addzone` and
|
||||||
|
TSIG `nsupdate` against the primary pod (the same write path external-dns uses).
|
||||||
|
Cluster-wide config — `options`, `controls`, ACLs, views, `dnssec-policy` blocks
|
||||||
|
and `response-policy` clauses — is rendered into a ConfigMap-backed `named.conf`
|
||||||
|
and reloaded with `rndc reconfig`. New zones land on the secondaries
|
||||||
|
automatically through a **catalog zone**, so secondaries never need
|
||||||
|
per-zone reconfiguration.
|
||||||
|
|
||||||
|
```
|
||||||
|
BindCluster ──> StatefulSet (pod-0 = primary, pod-N = secondaries)
|
||||||
|
├─ headless Service (pod-0.<cluster>-headless.<ns>.svc…)
|
||||||
|
├─ client Service (ClusterIP / LoadBalancer)
|
||||||
|
├─ ConfigMap (named.conf.primary / .secondary + entrypoint)
|
||||||
|
├─ Secret <cluster>-keys (TSIG key clauses, included by named.conf)
|
||||||
|
└─ Secret <cluster>-rndc (rndc control key)
|
||||||
|
|
||||||
|
BindZone / DNSRecord ──rndc addzone + nsupdate──> primary ──catalog + AXFR──> secondaries
|
||||||
|
```
|
||||||
|
|
||||||
|
The named.conf is rendered in two variants (primary/secondary); an entrypoint
|
||||||
|
script picks one based on the pod ordinal.
|
||||||
|
|
||||||
|
## Custom Resources
|
||||||
|
|
||||||
|
| Kind | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `BindCluster` | A set of BIND9 servers. `spec.mode`: `authoritative`, `resolver`, or `dynamic`. |
|
||||||
|
| `BindZone` | A forward/reverse zone (`primary`/`secondary`/`forward`/`stub`), records inline, optional dynamic-update + DNSSEC + catalog membership. |
|
||||||
|
| `DNSRecord` | A single record set applied via TSIG `nsupdate` — external-dns as a CRD. |
|
||||||
|
| `BindView` | A split-horizon view (`match-clients`, ordering, per-view recursion). |
|
||||||
|
| `BindTSIGKey` | A TSIG key; the operator generates material into a Secret (never stored in the CR). |
|
||||||
|
| `BindACL` | A reusable named `address_match_list`. |
|
||||||
|
| `BindCatalogZone` | A BIND catalog zone so secondaries auto-provision member zones. |
|
||||||
|
| `BindPolicy` | A Response Policy Zone (RPZ) / DNS firewall. |
|
||||||
|
| `BindDNSSECPolicy` | A `dnssec-policy` for automated signing. |
|
||||||
|
|
||||||
|
See `config/samples/` for worked examples.
|
||||||
|
|
||||||
|
## Migration mapping
|
||||||
|
|
||||||
|
The three Puppet-managed BIND roles map onto three `BindCluster`s:
|
||||||
|
|
||||||
|
| Puppet role | `BindCluster` | Mode |
|
||||||
|
|-------------|---------------|------|
|
||||||
|
| 3× authoritative masters | `auth` | `authoritative` (pod-0 primary, 2 secondaries) |
|
||||||
|
| 3× only-resolvers | `resolver` | `resolver` (3 identical recursive servers) |
|
||||||
|
| 3× external-dns | `externaldns` | `dynamic` (RFC2136 TSIG updates on primary) |
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make generate # regenerate deepcopy, CRDs and RBAC from kubebuilder markers
|
||||||
|
make build # build the operator binary
|
||||||
|
make test # go test -race
|
||||||
|
make lint fmt # go vet / gofmt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Local (kind)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
kind create cluster --name bind
|
||||||
|
docker build -t bind-operator:dev -f Dockerfile.operator .
|
||||||
|
kind load docker-image bind-operator:dev --name bind
|
||||||
|
|
||||||
|
kubectl apply -f config/crd/bases/
|
||||||
|
kubectl apply -f hack/kind/manifests/
|
||||||
|
kubectl apply -f config/samples/
|
||||||
|
```
|
||||||
|
|
||||||
|
## CI
|
||||||
|
|
||||||
|
Woodpecker runs `pre-commit` (gofmt + vet), `test`, and a dry-run image `build`
|
||||||
|
on pull requests; pushing a `v*` tag builds and pushes
|
||||||
|
`git.unkin.net/unkin/bind-operator` to the Gitea registry.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
- 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;
|
||||||
|
QNAME and NSDNAME triggers are fully supported.
|
||||||
|
|||||||
@@ -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{})
|
||||||
|
}
|
||||||
@@ -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{})
|
||||||
|
}
|
||||||
@@ -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{})
|
||||||
|
}
|
||||||
@@ -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{})
|
||||||
|
}
|
||||||
@@ -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{})
|
||||||
|
}
|
||||||
@@ -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{})
|
||||||
|
}
|
||||||
@@ -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{})
|
||||||
|
}
|
||||||
@@ -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{})
|
||||||
|
}
|
||||||
@@ -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{})
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
// +kubebuilder:object:generate=true
|
||||||
|
// +groupName=bind.unkin.net
|
||||||
|
package v1alpha1
|
||||||
@@ -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
@@ -0,0 +1,78 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
|
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/healthz"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||||
|
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
|
||||||
|
|
||||||
|
bindv1alpha1 "git.unkin.net/unkin/bind-operator/api/v1alpha1"
|
||||||
|
"git.unkin.net/unkin/bind-operator/internal/bind"
|
||||||
|
"git.unkin.net/unkin/bind-operator/internal/controller"
|
||||||
|
)
|
||||||
|
|
||||||
|
var scheme = runtime.NewScheme()
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
|
||||||
|
utilruntime.Must(bindv1alpha1.AddToScheme(scheme))
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var metricsAddr, probeAddr string
|
||||||
|
var leaderElect bool
|
||||||
|
|
||||||
|
flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "metrics endpoint address")
|
||||||
|
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "health probe address")
|
||||||
|
flag.BoolVar(&leaderElect, "leader-elect", false, "enable leader election")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
ctrl.SetLogger(zap.New(zap.UseDevMode(false)))
|
||||||
|
logger := ctrl.Log.WithName("setup")
|
||||||
|
|
||||||
|
restConfig := ctrl.GetConfigOrDie()
|
||||||
|
|
||||||
|
mgr, err := ctrl.NewManager(restConfig, ctrl.Options{
|
||||||
|
Scheme: scheme,
|
||||||
|
Metrics: metricsserver.Options{BindAddress: metricsAddr},
|
||||||
|
HealthProbeBindAddress: probeAddr,
|
||||||
|
LeaderElection: leaderElect,
|
||||||
|
LeaderElectionID: "bind-operator",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err, "unable to create manager")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
executor, err := bind.NewExecutor(restConfig)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err, "unable to create pod executor")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := controller.SetupAll(mgr, executor); err != nil {
|
||||||
|
logger.Error(err, "unable to set up controllers")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
|
||||||
|
logger.Error(err, "unable to set up health check")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
|
||||||
|
logger.Error(err, "unable to set up ready check")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("starting bind-operator")
|
||||||
|
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
|
||||||
|
logger.Error(err, "manager exited with error")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
---
|
||||||
|
apiVersion: apiextensions.k8s.io/v1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
controller-gen.kubebuilder.io/version: v0.17.3
|
||||||
|
name: bindacls.bind.unkin.net
|
||||||
|
spec:
|
||||||
|
group: bind.unkin.net
|
||||||
|
names:
|
||||||
|
kind: BindACL
|
||||||
|
listKind: BindACLList
|
||||||
|
plural: bindacls
|
||||||
|
shortNames:
|
||||||
|
- bacl
|
||||||
|
singular: bindacl
|
||||||
|
scope: Namespaced
|
||||||
|
versions:
|
||||||
|
- additionalPrinterColumns:
|
||||||
|
- jsonPath: .spec.clusterRef
|
||||||
|
name: Cluster
|
||||||
|
type: string
|
||||||
|
- jsonPath: .spec.entries[*]
|
||||||
|
name: Entries
|
||||||
|
type: integer
|
||||||
|
- jsonPath: .status.ready
|
||||||
|
name: Ready
|
||||||
|
type: boolean
|
||||||
|
name: v1alpha1
|
||||||
|
schema:
|
||||||
|
openAPIV3Schema:
|
||||||
|
description: |-
|
||||||
|
BindACL is a named address-match-list referenced by views, zones and
|
||||||
|
policies for match-clients / allow-query / allow-transfer / allow-update.
|
||||||
|
properties:
|
||||||
|
apiVersion:
|
||||||
|
description: |-
|
||||||
|
APIVersion defines the versioned schema of this representation of an object.
|
||||||
|
Servers should convert recognized schemas to the latest internal value, and
|
||||||
|
may reject unrecognized values.
|
||||||
|
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||||
|
type: string
|
||||||
|
kind:
|
||||||
|
description: |-
|
||||||
|
Kind is a string value representing the REST resource this object represents.
|
||||||
|
Servers may infer this from the endpoint the client submits requests to.
|
||||||
|
Cannot be updated.
|
||||||
|
In CamelCase.
|
||||||
|
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||||
|
type: string
|
||||||
|
metadata:
|
||||||
|
type: object
|
||||||
|
spec:
|
||||||
|
description: BindACLSpec defines a reusable named address_match_list.
|
||||||
|
properties:
|
||||||
|
clusterRef:
|
||||||
|
description: |-
|
||||||
|
ClusterRef names the BindCluster whose named.conf this ACL is rendered
|
||||||
|
into. When empty the ACL is available to every cluster in the namespace.
|
||||||
|
type: string
|
||||||
|
entries:
|
||||||
|
description: |-
|
||||||
|
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.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
minItems: 1
|
||||||
|
type: array
|
||||||
|
required:
|
||||||
|
- entries
|
||||||
|
type: object
|
||||||
|
status:
|
||||||
|
description: BindACLStatus reports observed ACL state.
|
||||||
|
properties:
|
||||||
|
conditions:
|
||||||
|
items:
|
||||||
|
description: Condition contains details for one aspect of the current
|
||||||
|
state of this API Resource.
|
||||||
|
properties:
|
||||||
|
lastTransitionTime:
|
||||||
|
description: |-
|
||||||
|
lastTransitionTime is the last time the condition transitioned from one status to another.
|
||||||
|
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
|
||||||
|
format: date-time
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
description: |-
|
||||||
|
message is a human readable message indicating details about the transition.
|
||||||
|
This may be an empty string.
|
||||||
|
maxLength: 32768
|
||||||
|
type: string
|
||||||
|
observedGeneration:
|
||||||
|
description: |-
|
||||||
|
observedGeneration represents the .metadata.generation that the condition was set based upon.
|
||||||
|
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
|
||||||
|
with respect to the current state of the instance.
|
||||||
|
format: int64
|
||||||
|
minimum: 0
|
||||||
|
type: integer
|
||||||
|
reason:
|
||||||
|
description: |-
|
||||||
|
reason contains a programmatic identifier indicating the reason for the condition's last transition.
|
||||||
|
Producers of specific condition types may define expected values and meanings for this field,
|
||||||
|
and whether the values are considered a guaranteed API.
|
||||||
|
The value should be a CamelCase string.
|
||||||
|
This field may not be empty.
|
||||||
|
maxLength: 1024
|
||||||
|
minLength: 1
|
||||||
|
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
description: status of the condition, one of True, False, Unknown.
|
||||||
|
enum:
|
||||||
|
- "True"
|
||||||
|
- "False"
|
||||||
|
- Unknown
|
||||||
|
type: string
|
||||||
|
type:
|
||||||
|
description: type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||||
|
maxLength: 316
|
||||||
|
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- lastTransitionTime
|
||||||
|
- message
|
||||||
|
- reason
|
||||||
|
- status
|
||||||
|
- type
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-map-keys:
|
||||||
|
- type
|
||||||
|
x-kubernetes-list-type: map
|
||||||
|
observedGeneration:
|
||||||
|
format: int64
|
||||||
|
type: integer
|
||||||
|
ready:
|
||||||
|
type: boolean
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
served: true
|
||||||
|
storage: true
|
||||||
|
subresources:
|
||||||
|
status: {}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
---
|
||||||
|
apiVersion: apiextensions.k8s.io/v1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
controller-gen.kubebuilder.io/version: v0.17.3
|
||||||
|
name: bindcatalogzones.bind.unkin.net
|
||||||
|
spec:
|
||||||
|
group: bind.unkin.net
|
||||||
|
names:
|
||||||
|
kind: BindCatalogZone
|
||||||
|
listKind: BindCatalogZoneList
|
||||||
|
plural: bindcatalogzones
|
||||||
|
shortNames:
|
||||||
|
- bcz
|
||||||
|
singular: bindcatalogzone
|
||||||
|
scope: Namespaced
|
||||||
|
versions:
|
||||||
|
- additionalPrinterColumns:
|
||||||
|
- jsonPath: .spec.clusterRef
|
||||||
|
name: Cluster
|
||||||
|
type: string
|
||||||
|
- jsonPath: .spec.zoneName
|
||||||
|
name: Zone
|
||||||
|
type: string
|
||||||
|
- jsonPath: .status.memberCount
|
||||||
|
name: Members
|
||||||
|
type: integer
|
||||||
|
- jsonPath: .status.ready
|
||||||
|
name: Ready
|
||||||
|
type: boolean
|
||||||
|
name: v1alpha1
|
||||||
|
schema:
|
||||||
|
openAPIV3Schema:
|
||||||
|
description: BindCatalogZone auto-provisions member zones onto cluster secondaries.
|
||||||
|
properties:
|
||||||
|
apiVersion:
|
||||||
|
description: |-
|
||||||
|
APIVersion defines the versioned schema of this representation of an object.
|
||||||
|
Servers should convert recognized schemas to the latest internal value, and
|
||||||
|
may reject unrecognized values.
|
||||||
|
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||||
|
type: string
|
||||||
|
kind:
|
||||||
|
description: |-
|
||||||
|
Kind is a string value representing the REST resource this object represents.
|
||||||
|
Servers may infer this from the endpoint the client submits requests to.
|
||||||
|
Cannot be updated.
|
||||||
|
In CamelCase.
|
||||||
|
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||||
|
type: string
|
||||||
|
metadata:
|
||||||
|
type: object
|
||||||
|
spec:
|
||||||
|
description: |-
|
||||||
|
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.
|
||||||
|
properties:
|
||||||
|
clusterRef:
|
||||||
|
description: ClusterRef names the owning BindCluster.
|
||||||
|
type: string
|
||||||
|
defaultPrimaries:
|
||||||
|
description: |-
|
||||||
|
DefaultPrimaries are the addresses member zones point at on secondaries.
|
||||||
|
Defaults to the cluster primary Service.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
transferKeyRef:
|
||||||
|
description: |-
|
||||||
|
TransferKeyRef names the BindTSIGKey authenticating catalog + member zone
|
||||||
|
transfers to secondaries.
|
||||||
|
type: string
|
||||||
|
zoneName:
|
||||||
|
description: ZoneName is the catalog zone's own origin, e.g. "catalog.internal".
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- clusterRef
|
||||||
|
- zoneName
|
||||||
|
type: object
|
||||||
|
status:
|
||||||
|
description: BindCatalogZoneStatus reports observed catalog state.
|
||||||
|
properties:
|
||||||
|
conditions:
|
||||||
|
items:
|
||||||
|
description: Condition contains details for one aspect of the current
|
||||||
|
state of this API Resource.
|
||||||
|
properties:
|
||||||
|
lastTransitionTime:
|
||||||
|
description: |-
|
||||||
|
lastTransitionTime is the last time the condition transitioned from one status to another.
|
||||||
|
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
|
||||||
|
format: date-time
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
description: |-
|
||||||
|
message is a human readable message indicating details about the transition.
|
||||||
|
This may be an empty string.
|
||||||
|
maxLength: 32768
|
||||||
|
type: string
|
||||||
|
observedGeneration:
|
||||||
|
description: |-
|
||||||
|
observedGeneration represents the .metadata.generation that the condition was set based upon.
|
||||||
|
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
|
||||||
|
with respect to the current state of the instance.
|
||||||
|
format: int64
|
||||||
|
minimum: 0
|
||||||
|
type: integer
|
||||||
|
reason:
|
||||||
|
description: |-
|
||||||
|
reason contains a programmatic identifier indicating the reason for the condition's last transition.
|
||||||
|
Producers of specific condition types may define expected values and meanings for this field,
|
||||||
|
and whether the values are considered a guaranteed API.
|
||||||
|
The value should be a CamelCase string.
|
||||||
|
This field may not be empty.
|
||||||
|
maxLength: 1024
|
||||||
|
minLength: 1
|
||||||
|
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
description: status of the condition, one of True, False, Unknown.
|
||||||
|
enum:
|
||||||
|
- "True"
|
||||||
|
- "False"
|
||||||
|
- Unknown
|
||||||
|
type: string
|
||||||
|
type:
|
||||||
|
description: type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||||
|
maxLength: 316
|
||||||
|
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- lastTransitionTime
|
||||||
|
- message
|
||||||
|
- reason
|
||||||
|
- status
|
||||||
|
- type
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-map-keys:
|
||||||
|
- type
|
||||||
|
x-kubernetes-list-type: map
|
||||||
|
memberCount:
|
||||||
|
description: MemberCount is the number of member zones registered
|
||||||
|
in the catalog.
|
||||||
|
format: int32
|
||||||
|
type: integer
|
||||||
|
observedGeneration:
|
||||||
|
format: int64
|
||||||
|
type: integer
|
||||||
|
ready:
|
||||||
|
type: boolean
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
served: true
|
||||||
|
storage: true
|
||||||
|
subresources:
|
||||||
|
status: {}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,219 @@
|
|||||||
|
---
|
||||||
|
apiVersion: apiextensions.k8s.io/v1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
controller-gen.kubebuilder.io/version: v0.17.3
|
||||||
|
name: binddnssecpolicies.bind.unkin.net
|
||||||
|
spec:
|
||||||
|
group: bind.unkin.net
|
||||||
|
names:
|
||||||
|
kind: BindDNSSECPolicy
|
||||||
|
listKind: BindDNSSECPolicyList
|
||||||
|
plural: binddnssecpolicies
|
||||||
|
shortNames:
|
||||||
|
- bdp
|
||||||
|
singular: binddnssecpolicy
|
||||||
|
scope: Namespaced
|
||||||
|
versions:
|
||||||
|
- additionalPrinterColumns:
|
||||||
|
- jsonPath: .spec.clusterRef
|
||||||
|
name: Cluster
|
||||||
|
type: string
|
||||||
|
- jsonPath: .spec.algorithm
|
||||||
|
name: Algorithm
|
||||||
|
type: string
|
||||||
|
- jsonPath: .status.zoneCount
|
||||||
|
name: Zones
|
||||||
|
type: integer
|
||||||
|
- jsonPath: .status.ready
|
||||||
|
name: Ready
|
||||||
|
type: boolean
|
||||||
|
name: v1alpha1
|
||||||
|
schema:
|
||||||
|
openAPIV3Schema:
|
||||||
|
description: BindDNSSECPolicy is a reusable DNSSEC signing policy.
|
||||||
|
properties:
|
||||||
|
apiVersion:
|
||||||
|
description: |-
|
||||||
|
APIVersion defines the versioned schema of this representation of an object.
|
||||||
|
Servers should convert recognized schemas to the latest internal value, and
|
||||||
|
may reject unrecognized values.
|
||||||
|
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||||
|
type: string
|
||||||
|
kind:
|
||||||
|
description: |-
|
||||||
|
Kind is a string value representing the REST resource this object represents.
|
||||||
|
Servers may infer this from the endpoint the client submits requests to.
|
||||||
|
Cannot be updated.
|
||||||
|
In CamelCase.
|
||||||
|
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||||
|
type: string
|
||||||
|
metadata:
|
||||||
|
type: object
|
||||||
|
spec:
|
||||||
|
description: |-
|
||||||
|
BindDNSSECPolicySpec mirrors a BIND9 dnssec-policy. Zones referencing it are
|
||||||
|
signed with inline-signing and automated key management.
|
||||||
|
properties:
|
||||||
|
algorithm:
|
||||||
|
default: ecdsap256sha256
|
||||||
|
description: Algorithm for signing. Defaults to ecdsap256sha256.
|
||||||
|
type: string
|
||||||
|
clusterRef:
|
||||||
|
description: ClusterRef names the owning BindCluster.
|
||||||
|
type: string
|
||||||
|
csk:
|
||||||
|
description: CSK, when set, uses a Combined Signing Key instead of
|
||||||
|
split KSK/ZSK.
|
||||||
|
properties:
|
||||||
|
algorithm:
|
||||||
|
description: Algorithm overrides the policy algorithm for this
|
||||||
|
key.
|
||||||
|
type: string
|
||||||
|
keySize:
|
||||||
|
description: KeySize in bits for RSA algorithms (ignored for ECDSA/EdDSA).
|
||||||
|
format: int32
|
||||||
|
type: integer
|
||||||
|
lifetime:
|
||||||
|
description: |-
|
||||||
|
Lifetime is how long the key is used before rollover, e.g. "P30D" or
|
||||||
|
"unlimited". Empty means unlimited.
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
extraOptions:
|
||||||
|
description: ExtraOptions are raw named.conf lines appended inside
|
||||||
|
the policy block.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
ksk:
|
||||||
|
description: KSK is the Key Signing Key configuration (ignored when
|
||||||
|
CSK is set).
|
||||||
|
properties:
|
||||||
|
algorithm:
|
||||||
|
description: Algorithm overrides the policy algorithm for this
|
||||||
|
key.
|
||||||
|
type: string
|
||||||
|
keySize:
|
||||||
|
description: KeySize in bits for RSA algorithms (ignored for ECDSA/EdDSA).
|
||||||
|
format: int32
|
||||||
|
type: integer
|
||||||
|
lifetime:
|
||||||
|
description: |-
|
||||||
|
Lifetime is how long the key is used before rollover, e.g. "P30D" or
|
||||||
|
"unlimited". Empty means unlimited.
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
maxZoneTTL:
|
||||||
|
description: MaxZoneTTL, e.g. "P1D".
|
||||||
|
type: string
|
||||||
|
nsec3:
|
||||||
|
description: NSEC3 enables NSEC3 hashing instead of NSEC.
|
||||||
|
type: boolean
|
||||||
|
policyName:
|
||||||
|
description: |-
|
||||||
|
PolicyName is the dnssec-policy name in named.conf. Defaults to the object
|
||||||
|
name.
|
||||||
|
type: string
|
||||||
|
signaturesValidity:
|
||||||
|
description: SignaturesValidity, e.g. "P14D".
|
||||||
|
type: string
|
||||||
|
zsk:
|
||||||
|
description: ZSK is the Zone Signing Key configuration (ignored when
|
||||||
|
CSK is set).
|
||||||
|
properties:
|
||||||
|
algorithm:
|
||||||
|
description: Algorithm overrides the policy algorithm for this
|
||||||
|
key.
|
||||||
|
type: string
|
||||||
|
keySize:
|
||||||
|
description: KeySize in bits for RSA algorithms (ignored for ECDSA/EdDSA).
|
||||||
|
format: int32
|
||||||
|
type: integer
|
||||||
|
lifetime:
|
||||||
|
description: |-
|
||||||
|
Lifetime is how long the key is used before rollover, e.g. "P30D" or
|
||||||
|
"unlimited". Empty means unlimited.
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- clusterRef
|
||||||
|
type: object
|
||||||
|
status:
|
||||||
|
description: BindDNSSECPolicyStatus reports observed policy state.
|
||||||
|
properties:
|
||||||
|
conditions:
|
||||||
|
items:
|
||||||
|
description: Condition contains details for one aspect of the current
|
||||||
|
state of this API Resource.
|
||||||
|
properties:
|
||||||
|
lastTransitionTime:
|
||||||
|
description: |-
|
||||||
|
lastTransitionTime is the last time the condition transitioned from one status to another.
|
||||||
|
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
|
||||||
|
format: date-time
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
description: |-
|
||||||
|
message is a human readable message indicating details about the transition.
|
||||||
|
This may be an empty string.
|
||||||
|
maxLength: 32768
|
||||||
|
type: string
|
||||||
|
observedGeneration:
|
||||||
|
description: |-
|
||||||
|
observedGeneration represents the .metadata.generation that the condition was set based upon.
|
||||||
|
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
|
||||||
|
with respect to the current state of the instance.
|
||||||
|
format: int64
|
||||||
|
minimum: 0
|
||||||
|
type: integer
|
||||||
|
reason:
|
||||||
|
description: |-
|
||||||
|
reason contains a programmatic identifier indicating the reason for the condition's last transition.
|
||||||
|
Producers of specific condition types may define expected values and meanings for this field,
|
||||||
|
and whether the values are considered a guaranteed API.
|
||||||
|
The value should be a CamelCase string.
|
||||||
|
This field may not be empty.
|
||||||
|
maxLength: 1024
|
||||||
|
minLength: 1
|
||||||
|
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
description: status of the condition, one of True, False, Unknown.
|
||||||
|
enum:
|
||||||
|
- "True"
|
||||||
|
- "False"
|
||||||
|
- Unknown
|
||||||
|
type: string
|
||||||
|
type:
|
||||||
|
description: type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||||
|
maxLength: 316
|
||||||
|
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- lastTransitionTime
|
||||||
|
- message
|
||||||
|
- reason
|
||||||
|
- status
|
||||||
|
- type
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-map-keys:
|
||||||
|
- type
|
||||||
|
x-kubernetes-list-type: map
|
||||||
|
observedGeneration:
|
||||||
|
format: int64
|
||||||
|
type: integer
|
||||||
|
ready:
|
||||||
|
type: boolean
|
||||||
|
zoneCount:
|
||||||
|
description: ZoneCount is the number of zones signed with this policy.
|
||||||
|
format: int32
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
served: true
|
||||||
|
storage: true
|
||||||
|
subresources:
|
||||||
|
status: {}
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
---
|
||||||
|
apiVersion: apiextensions.k8s.io/v1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
controller-gen.kubebuilder.io/version: v0.17.3
|
||||||
|
name: bindpolicies.bind.unkin.net
|
||||||
|
spec:
|
||||||
|
group: bind.unkin.net
|
||||||
|
names:
|
||||||
|
kind: BindPolicy
|
||||||
|
listKind: BindPolicyList
|
||||||
|
plural: bindpolicies
|
||||||
|
shortNames:
|
||||||
|
- bp
|
||||||
|
singular: bindpolicy
|
||||||
|
scope: Namespaced
|
||||||
|
versions:
|
||||||
|
- additionalPrinterColumns:
|
||||||
|
- jsonPath: .spec.clusterRef
|
||||||
|
name: Cluster
|
||||||
|
type: string
|
||||||
|
- jsonPath: .spec.zoneName
|
||||||
|
name: Zone
|
||||||
|
type: string
|
||||||
|
- jsonPath: .status.ruleCount
|
||||||
|
name: Rules
|
||||||
|
type: integer
|
||||||
|
- jsonPath: .status.ready
|
||||||
|
name: Ready
|
||||||
|
type: boolean
|
||||||
|
name: v1alpha1
|
||||||
|
schema:
|
||||||
|
openAPIV3Schema:
|
||||||
|
description: BindPolicy is a Response Policy Zone (RPZ) applied to a cluster.
|
||||||
|
properties:
|
||||||
|
apiVersion:
|
||||||
|
description: |-
|
||||||
|
APIVersion defines the versioned schema of this representation of an object.
|
||||||
|
Servers should convert recognized schemas to the latest internal value, and
|
||||||
|
may reject unrecognized values.
|
||||||
|
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||||
|
type: string
|
||||||
|
kind:
|
||||||
|
description: |-
|
||||||
|
Kind is a string value representing the REST resource this object represents.
|
||||||
|
Servers may infer this from the endpoint the client submits requests to.
|
||||||
|
Cannot be updated.
|
||||||
|
In CamelCase.
|
||||||
|
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||||
|
type: string
|
||||||
|
metadata:
|
||||||
|
type: object
|
||||||
|
spec:
|
||||||
|
description: |-
|
||||||
|
BindPolicySpec defines a Response Policy Zone (RPZ) — a DNS firewall applied
|
||||||
|
to a resolver cluster.
|
||||||
|
properties:
|
||||||
|
clusterRef:
|
||||||
|
description: ClusterRef names the owning BindCluster (typically a
|
||||||
|
resolver).
|
||||||
|
type: string
|
||||||
|
order:
|
||||||
|
default: 100
|
||||||
|
description: Order controls this policy's position in the response-policy
|
||||||
|
clause.
|
||||||
|
format: int32
|
||||||
|
type: integer
|
||||||
|
primaries:
|
||||||
|
description: |-
|
||||||
|
Primaries lets the RPZ zone be transferred from an external feed instead
|
||||||
|
of being locally populated.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
rules:
|
||||||
|
description: Rules are the inline policy triggers.
|
||||||
|
items:
|
||||||
|
description: RPZRule is a single response-policy rule.
|
||||||
|
properties:
|
||||||
|
action:
|
||||||
|
default: nxdomain
|
||||||
|
description: Action taken when the rule matches.
|
||||||
|
enum:
|
||||||
|
- nxdomain
|
||||||
|
- nodata
|
||||||
|
- passthru
|
||||||
|
- drop
|
||||||
|
- tcp-only
|
||||||
|
- cname
|
||||||
|
type: string
|
||||||
|
match:
|
||||||
|
description: Match is the trigger value, e.g. a domain "bad.example."
|
||||||
|
or CIDR.
|
||||||
|
type: string
|
||||||
|
target:
|
||||||
|
description: Target is the rewrite target when Action is cname.
|
||||||
|
type: string
|
||||||
|
trigger:
|
||||||
|
default: qname
|
||||||
|
description: Trigger selects what the Match is compared against.
|
||||||
|
enum:
|
||||||
|
- qname
|
||||||
|
- client-ip
|
||||||
|
- ip
|
||||||
|
- nsdname
|
||||||
|
- nsip
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- match
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
transferKeyRef:
|
||||||
|
description: TransferKeyRef names the BindTSIGKey used to pull from
|
||||||
|
Primaries.
|
||||||
|
type: string
|
||||||
|
viewRef:
|
||||||
|
description: ViewRef optionally scopes the policy to a single view.
|
||||||
|
type: string
|
||||||
|
zoneName:
|
||||||
|
description: ZoneName is the RPZ zone origin, e.g. "rpz.internal".
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- clusterRef
|
||||||
|
- zoneName
|
||||||
|
type: object
|
||||||
|
status:
|
||||||
|
description: BindPolicyStatus reports observed policy state.
|
||||||
|
properties:
|
||||||
|
conditions:
|
||||||
|
items:
|
||||||
|
description: Condition contains details for one aspect of the current
|
||||||
|
state of this API Resource.
|
||||||
|
properties:
|
||||||
|
lastTransitionTime:
|
||||||
|
description: |-
|
||||||
|
lastTransitionTime is the last time the condition transitioned from one status to another.
|
||||||
|
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
|
||||||
|
format: date-time
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
description: |-
|
||||||
|
message is a human readable message indicating details about the transition.
|
||||||
|
This may be an empty string.
|
||||||
|
maxLength: 32768
|
||||||
|
type: string
|
||||||
|
observedGeneration:
|
||||||
|
description: |-
|
||||||
|
observedGeneration represents the .metadata.generation that the condition was set based upon.
|
||||||
|
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
|
||||||
|
with respect to the current state of the instance.
|
||||||
|
format: int64
|
||||||
|
minimum: 0
|
||||||
|
type: integer
|
||||||
|
reason:
|
||||||
|
description: |-
|
||||||
|
reason contains a programmatic identifier indicating the reason for the condition's last transition.
|
||||||
|
Producers of specific condition types may define expected values and meanings for this field,
|
||||||
|
and whether the values are considered a guaranteed API.
|
||||||
|
The value should be a CamelCase string.
|
||||||
|
This field may not be empty.
|
||||||
|
maxLength: 1024
|
||||||
|
minLength: 1
|
||||||
|
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
description: status of the condition, one of True, False, Unknown.
|
||||||
|
enum:
|
||||||
|
- "True"
|
||||||
|
- "False"
|
||||||
|
- Unknown
|
||||||
|
type: string
|
||||||
|
type:
|
||||||
|
description: type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||||
|
maxLength: 316
|
||||||
|
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- lastTransitionTime
|
||||||
|
- message
|
||||||
|
- reason
|
||||||
|
- status
|
||||||
|
- type
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-map-keys:
|
||||||
|
- type
|
||||||
|
x-kubernetes-list-type: map
|
||||||
|
observedGeneration:
|
||||||
|
format: int64
|
||||||
|
type: integer
|
||||||
|
ready:
|
||||||
|
type: boolean
|
||||||
|
ruleCount:
|
||||||
|
description: RuleCount is the number of active rules.
|
||||||
|
format: int32
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
served: true
|
||||||
|
storage: true
|
||||||
|
subresources:
|
||||||
|
status: {}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
---
|
||||||
|
apiVersion: apiextensions.k8s.io/v1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
controller-gen.kubebuilder.io/version: v0.17.3
|
||||||
|
name: bindtsigkeys.bind.unkin.net
|
||||||
|
spec:
|
||||||
|
group: bind.unkin.net
|
||||||
|
names:
|
||||||
|
kind: BindTSIGKey
|
||||||
|
listKind: BindTSIGKeyList
|
||||||
|
plural: bindtsigkeys
|
||||||
|
shortNames:
|
||||||
|
- btk
|
||||||
|
singular: bindtsigkey
|
||||||
|
scope: Namespaced
|
||||||
|
versions:
|
||||||
|
- additionalPrinterColumns:
|
||||||
|
- jsonPath: .spec.algorithm
|
||||||
|
name: Algorithm
|
||||||
|
type: string
|
||||||
|
- jsonPath: .status.secretName
|
||||||
|
name: Secret
|
||||||
|
type: string
|
||||||
|
- jsonPath: .status.ready
|
||||||
|
name: Ready
|
||||||
|
type: boolean
|
||||||
|
name: v1alpha1
|
||||||
|
schema:
|
||||||
|
openAPIV3Schema:
|
||||||
|
description: |-
|
||||||
|
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.
|
||||||
|
properties:
|
||||||
|
apiVersion:
|
||||||
|
description: |-
|
||||||
|
APIVersion defines the versioned schema of this representation of an object.
|
||||||
|
Servers should convert recognized schemas to the latest internal value, and
|
||||||
|
may reject unrecognized values.
|
||||||
|
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||||
|
type: string
|
||||||
|
kind:
|
||||||
|
description: |-
|
||||||
|
Kind is a string value representing the REST resource this object represents.
|
||||||
|
Servers may infer this from the endpoint the client submits requests to.
|
||||||
|
Cannot be updated.
|
||||||
|
In CamelCase.
|
||||||
|
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||||
|
type: string
|
||||||
|
metadata:
|
||||||
|
type: object
|
||||||
|
spec:
|
||||||
|
description: |-
|
||||||
|
BindTSIGKeySpec defines a TSIG key. If no existing key material is imported,
|
||||||
|
the operator generates a random key and stores it in a Secret.
|
||||||
|
properties:
|
||||||
|
algorithm:
|
||||||
|
default: hmac-sha256
|
||||||
|
description: Algorithm is the HMAC algorithm. Defaults to hmac-sha256.
|
||||||
|
enum:
|
||||||
|
- hmac-sha256
|
||||||
|
- hmac-sha512
|
||||||
|
- hmac-sha384
|
||||||
|
- hmac-sha224
|
||||||
|
- hmac-sha1
|
||||||
|
- hmac-md5
|
||||||
|
type: string
|
||||||
|
importExisting:
|
||||||
|
description: |-
|
||||||
|
ImportExisting, when true, means the referenced Secret already contains a
|
||||||
|
`secret` key and the operator will not generate new material.
|
||||||
|
type: boolean
|
||||||
|
keyName:
|
||||||
|
description: |-
|
||||||
|
KeyName is the TSIG key name emitted into named.conf. Defaults to the
|
||||||
|
object name.
|
||||||
|
type: string
|
||||||
|
secretName:
|
||||||
|
description: |-
|
||||||
|
SecretName is the Secret the key material is written to (or read from when
|
||||||
|
ImportExisting is set). Defaults to "<name>-tsig".
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
status:
|
||||||
|
description: BindTSIGKeyStatus reports observed TSIG key state.
|
||||||
|
properties:
|
||||||
|
conditions:
|
||||||
|
items:
|
||||||
|
description: Condition contains details for one aspect of the current
|
||||||
|
state of this API Resource.
|
||||||
|
properties:
|
||||||
|
lastTransitionTime:
|
||||||
|
description: |-
|
||||||
|
lastTransitionTime is the last time the condition transitioned from one status to another.
|
||||||
|
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
|
||||||
|
format: date-time
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
description: |-
|
||||||
|
message is a human readable message indicating details about the transition.
|
||||||
|
This may be an empty string.
|
||||||
|
maxLength: 32768
|
||||||
|
type: string
|
||||||
|
observedGeneration:
|
||||||
|
description: |-
|
||||||
|
observedGeneration represents the .metadata.generation that the condition was set based upon.
|
||||||
|
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
|
||||||
|
with respect to the current state of the instance.
|
||||||
|
format: int64
|
||||||
|
minimum: 0
|
||||||
|
type: integer
|
||||||
|
reason:
|
||||||
|
description: |-
|
||||||
|
reason contains a programmatic identifier indicating the reason for the condition's last transition.
|
||||||
|
Producers of specific condition types may define expected values and meanings for this field,
|
||||||
|
and whether the values are considered a guaranteed API.
|
||||||
|
The value should be a CamelCase string.
|
||||||
|
This field may not be empty.
|
||||||
|
maxLength: 1024
|
||||||
|
minLength: 1
|
||||||
|
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
description: status of the condition, one of True, False, Unknown.
|
||||||
|
enum:
|
||||||
|
- "True"
|
||||||
|
- "False"
|
||||||
|
- Unknown
|
||||||
|
type: string
|
||||||
|
type:
|
||||||
|
description: type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||||
|
maxLength: 316
|
||||||
|
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- lastTransitionTime
|
||||||
|
- message
|
||||||
|
- reason
|
||||||
|
- status
|
||||||
|
- type
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-map-keys:
|
||||||
|
- type
|
||||||
|
x-kubernetes-list-type: map
|
||||||
|
keyName:
|
||||||
|
description: KeyName as used in named.conf.
|
||||||
|
type: string
|
||||||
|
observedGeneration:
|
||||||
|
description: ObservedGeneration is the last reconciled generation.
|
||||||
|
format: int64
|
||||||
|
type: integer
|
||||||
|
ready:
|
||||||
|
description: Ready is true once the key Secret exists.
|
||||||
|
type: boolean
|
||||||
|
secretName:
|
||||||
|
description: SecretName holds the generated/managed key material.
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
served: true
|
||||||
|
storage: true
|
||||||
|
subresources:
|
||||||
|
status: {}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
---
|
||||||
|
apiVersion: apiextensions.k8s.io/v1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
controller-gen.kubebuilder.io/version: v0.17.3
|
||||||
|
name: bindviews.bind.unkin.net
|
||||||
|
spec:
|
||||||
|
group: bind.unkin.net
|
||||||
|
names:
|
||||||
|
kind: BindView
|
||||||
|
listKind: BindViewList
|
||||||
|
plural: bindviews
|
||||||
|
shortNames:
|
||||||
|
- bv
|
||||||
|
singular: bindview
|
||||||
|
scope: Namespaced
|
||||||
|
versions:
|
||||||
|
- additionalPrinterColumns:
|
||||||
|
- jsonPath: .spec.clusterRef
|
||||||
|
name: Cluster
|
||||||
|
type: string
|
||||||
|
- jsonPath: .spec.order
|
||||||
|
name: Order
|
||||||
|
type: integer
|
||||||
|
- jsonPath: .status.zoneCount
|
||||||
|
name: Zones
|
||||||
|
type: integer
|
||||||
|
- jsonPath: .status.ready
|
||||||
|
name: Ready
|
||||||
|
type: boolean
|
||||||
|
name: v1alpha1
|
||||||
|
schema:
|
||||||
|
openAPIV3Schema:
|
||||||
|
description: BindView is a split-horizon view on a BindCluster.
|
||||||
|
properties:
|
||||||
|
apiVersion:
|
||||||
|
description: |-
|
||||||
|
APIVersion defines the versioned schema of this representation of an object.
|
||||||
|
Servers should convert recognized schemas to the latest internal value, and
|
||||||
|
may reject unrecognized values.
|
||||||
|
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||||
|
type: string
|
||||||
|
kind:
|
||||||
|
description: |-
|
||||||
|
Kind is a string value representing the REST resource this object represents.
|
||||||
|
Servers may infer this from the endpoint the client submits requests to.
|
||||||
|
Cannot be updated.
|
||||||
|
In CamelCase.
|
||||||
|
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||||
|
type: string
|
||||||
|
metadata:
|
||||||
|
type: object
|
||||||
|
spec:
|
||||||
|
description: |-
|
||||||
|
BindViewSpec defines a split-horizon view. View ordering is significant in
|
||||||
|
BIND; use Order to control the sequence in named.conf.
|
||||||
|
properties:
|
||||||
|
allowQuery:
|
||||||
|
description: AllowQuery is an address-match-list restricting queries
|
||||||
|
into this view.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
clusterRef:
|
||||||
|
description: ClusterRef names the owning BindCluster.
|
||||||
|
type: string
|
||||||
|
extraOptions:
|
||||||
|
description: ExtraOptions are raw named.conf lines appended inside
|
||||||
|
the view block.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
matchClients:
|
||||||
|
description: |-
|
||||||
|
MatchClients is an address-match-list (inline entries and/or ACL names)
|
||||||
|
selecting which clients this view answers. Defaults to "any".
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
matchDestinations:
|
||||||
|
description: MatchDestinations is an optional destination address-match-list.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
order:
|
||||||
|
default: 100
|
||||||
|
description: |-
|
||||||
|
Order controls the position of this view in named.conf (ascending). The
|
||||||
|
first view whose match-clients matches a query wins.
|
||||||
|
format: int32
|
||||||
|
type: integer
|
||||||
|
recursion:
|
||||||
|
description: Recursion overrides the cluster recursion setting for
|
||||||
|
this view.
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- clusterRef
|
||||||
|
type: object
|
||||||
|
status:
|
||||||
|
description: BindViewStatus reports observed view state.
|
||||||
|
properties:
|
||||||
|
conditions:
|
||||||
|
items:
|
||||||
|
description: Condition contains details for one aspect of the current
|
||||||
|
state of this API Resource.
|
||||||
|
properties:
|
||||||
|
lastTransitionTime:
|
||||||
|
description: |-
|
||||||
|
lastTransitionTime is the last time the condition transitioned from one status to another.
|
||||||
|
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
|
||||||
|
format: date-time
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
description: |-
|
||||||
|
message is a human readable message indicating details about the transition.
|
||||||
|
This may be an empty string.
|
||||||
|
maxLength: 32768
|
||||||
|
type: string
|
||||||
|
observedGeneration:
|
||||||
|
description: |-
|
||||||
|
observedGeneration represents the .metadata.generation that the condition was set based upon.
|
||||||
|
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
|
||||||
|
with respect to the current state of the instance.
|
||||||
|
format: int64
|
||||||
|
minimum: 0
|
||||||
|
type: integer
|
||||||
|
reason:
|
||||||
|
description: |-
|
||||||
|
reason contains a programmatic identifier indicating the reason for the condition's last transition.
|
||||||
|
Producers of specific condition types may define expected values and meanings for this field,
|
||||||
|
and whether the values are considered a guaranteed API.
|
||||||
|
The value should be a CamelCase string.
|
||||||
|
This field may not be empty.
|
||||||
|
maxLength: 1024
|
||||||
|
minLength: 1
|
||||||
|
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
description: status of the condition, one of True, False, Unknown.
|
||||||
|
enum:
|
||||||
|
- "True"
|
||||||
|
- "False"
|
||||||
|
- Unknown
|
||||||
|
type: string
|
||||||
|
type:
|
||||||
|
description: type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||||
|
maxLength: 316
|
||||||
|
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- lastTransitionTime
|
||||||
|
- message
|
||||||
|
- reason
|
||||||
|
- status
|
||||||
|
- type
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-map-keys:
|
||||||
|
- type
|
||||||
|
x-kubernetes-list-type: map
|
||||||
|
observedGeneration:
|
||||||
|
format: int64
|
||||||
|
type: integer
|
||||||
|
ready:
|
||||||
|
type: boolean
|
||||||
|
zoneCount:
|
||||||
|
description: ZoneCount is the number of zones currently bound to this
|
||||||
|
view.
|
||||||
|
format: int32
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
served: true
|
||||||
|
storage: true
|
||||||
|
subresources:
|
||||||
|
status: {}
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
---
|
||||||
|
apiVersion: apiextensions.k8s.io/v1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
controller-gen.kubebuilder.io/version: v0.17.3
|
||||||
|
name: bindzones.bind.unkin.net
|
||||||
|
spec:
|
||||||
|
group: bind.unkin.net
|
||||||
|
names:
|
||||||
|
kind: BindZone
|
||||||
|
listKind: BindZoneList
|
||||||
|
plural: bindzones
|
||||||
|
shortNames:
|
||||||
|
- bz
|
||||||
|
singular: bindzone
|
||||||
|
scope: Namespaced
|
||||||
|
versions:
|
||||||
|
- additionalPrinterColumns:
|
||||||
|
- jsonPath: .spec.zoneName
|
||||||
|
name: Zone
|
||||||
|
type: string
|
||||||
|
- jsonPath: .spec.type
|
||||||
|
name: Type
|
||||||
|
type: string
|
||||||
|
- jsonPath: .spec.clusterRef
|
||||||
|
name: Cluster
|
||||||
|
type: string
|
||||||
|
- jsonPath: .status.serial
|
||||||
|
name: Serial
|
||||||
|
type: integer
|
||||||
|
- jsonPath: .status.phase
|
||||||
|
name: Phase
|
||||||
|
type: string
|
||||||
|
name: v1alpha1
|
||||||
|
schema:
|
||||||
|
openAPIV3Schema:
|
||||||
|
description: BindZone is a forward or reverse DNS zone.
|
||||||
|
properties:
|
||||||
|
apiVersion:
|
||||||
|
description: |-
|
||||||
|
APIVersion defines the versioned schema of this representation of an object.
|
||||||
|
Servers should convert recognized schemas to the latest internal value, and
|
||||||
|
may reject unrecognized values.
|
||||||
|
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||||
|
type: string
|
||||||
|
kind:
|
||||||
|
description: |-
|
||||||
|
Kind is a string value representing the REST resource this object represents.
|
||||||
|
Servers may infer this from the endpoint the client submits requests to.
|
||||||
|
Cannot be updated.
|
||||||
|
In CamelCase.
|
||||||
|
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||||
|
type: string
|
||||||
|
metadata:
|
||||||
|
type: object
|
||||||
|
spec:
|
||||||
|
description: BindZoneSpec defines a DNS zone managed on a BindCluster's
|
||||||
|
primary.
|
||||||
|
properties:
|
||||||
|
allowTransfer:
|
||||||
|
description: |-
|
||||||
|
AllowTransfer is an address-match-list (inline entries and/or ACL/key
|
||||||
|
names) permitted to AXFR/IXFR this zone.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
catalog:
|
||||||
|
default: true
|
||||||
|
description: |-
|
||||||
|
Catalog, when true, registers this zone as a member of the cluster's
|
||||||
|
catalog zone so secondaries auto-provision it.
|
||||||
|
type: boolean
|
||||||
|
clusterRef:
|
||||||
|
description: ClusterRef names the owning BindCluster.
|
||||||
|
type: string
|
||||||
|
defaultTTL:
|
||||||
|
default: 3600
|
||||||
|
description: DefaultTTL for records that do not set their own TTL.
|
||||||
|
Defaults to 3600.
|
||||||
|
format: int32
|
||||||
|
type: integer
|
||||||
|
dnssecPolicyRef:
|
||||||
|
description: DNSSECPolicyRef names a BindDNSSECPolicy to sign this
|
||||||
|
zone with.
|
||||||
|
type: string
|
||||||
|
dynamicUpdate:
|
||||||
|
description: |-
|
||||||
|
DynamicUpdate enables RFC2136 updates for this zone (external-dns style).
|
||||||
|
When true, UpdateKeyRef must reference a BindTSIGKey.
|
||||||
|
type: boolean
|
||||||
|
forwarders:
|
||||||
|
description: Forwarders lists upstreams for a forward-type zone.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
primaries:
|
||||||
|
description: Primaries lists source servers for a secondary/stub-type
|
||||||
|
zone.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
records:
|
||||||
|
description: Records are static record sets seeded into a primary
|
||||||
|
zone.
|
||||||
|
items:
|
||||||
|
description: |-
|
||||||
|
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.
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
default: '@'
|
||||||
|
description: |-
|
||||||
|
Name is the owner name, relative to the zone apex or fully qualified.
|
||||||
|
Use "@" for the apex.
|
||||||
|
type: string
|
||||||
|
ttl:
|
||||||
|
description: TTL for the record set in seconds. Falls back to
|
||||||
|
the zone default TTL.
|
||||||
|
format: int32
|
||||||
|
type: integer
|
||||||
|
type:
|
||||||
|
description: Type is the RR type, e.g. A, AAAA, CNAME, MX, TXT,
|
||||||
|
SRV, NS, PTR, CAA.
|
||||||
|
type: string
|
||||||
|
values:
|
||||||
|
description: |-
|
||||||
|
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.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
minItems: 1
|
||||||
|
type: array
|
||||||
|
required:
|
||||||
|
- type
|
||||||
|
- values
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
transferKeyRef:
|
||||||
|
description: |-
|
||||||
|
TransferKeyRef names the BindTSIGKey used to authenticate transfers from
|
||||||
|
Primaries for a secondary zone.
|
||||||
|
type: string
|
||||||
|
type:
|
||||||
|
default: primary
|
||||||
|
description: Type is the zone type. Defaults to primary.
|
||||||
|
enum:
|
||||||
|
- primary
|
||||||
|
- secondary
|
||||||
|
- forward
|
||||||
|
- stub
|
||||||
|
type: string
|
||||||
|
updateKeyRef:
|
||||||
|
description: UpdateKeyRef names the BindTSIGKey permitted to send
|
||||||
|
dynamic updates.
|
||||||
|
type: string
|
||||||
|
viewRef:
|
||||||
|
description: ViewRef optionally binds this zone to a BindView.
|
||||||
|
type: string
|
||||||
|
zoneName:
|
||||||
|
description: ZoneName is the DNS origin, e.g. "example.com" or "2.0.192.in-addr.arpa".
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- clusterRef
|
||||||
|
- zoneName
|
||||||
|
type: object
|
||||||
|
status:
|
||||||
|
description: BindZoneStatus reports observed zone state.
|
||||||
|
properties:
|
||||||
|
conditions:
|
||||||
|
items:
|
||||||
|
description: Condition contains details for one aspect of the current
|
||||||
|
state of this API Resource.
|
||||||
|
properties:
|
||||||
|
lastTransitionTime:
|
||||||
|
description: |-
|
||||||
|
lastTransitionTime is the last time the condition transitioned from one status to another.
|
||||||
|
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
|
||||||
|
format: date-time
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
description: |-
|
||||||
|
message is a human readable message indicating details about the transition.
|
||||||
|
This may be an empty string.
|
||||||
|
maxLength: 32768
|
||||||
|
type: string
|
||||||
|
observedGeneration:
|
||||||
|
description: |-
|
||||||
|
observedGeneration represents the .metadata.generation that the condition was set based upon.
|
||||||
|
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
|
||||||
|
with respect to the current state of the instance.
|
||||||
|
format: int64
|
||||||
|
minimum: 0
|
||||||
|
type: integer
|
||||||
|
reason:
|
||||||
|
description: |-
|
||||||
|
reason contains a programmatic identifier indicating the reason for the condition's last transition.
|
||||||
|
Producers of specific condition types may define expected values and meanings for this field,
|
||||||
|
and whether the values are considered a guaranteed API.
|
||||||
|
The value should be a CamelCase string.
|
||||||
|
This field may not be empty.
|
||||||
|
maxLength: 1024
|
||||||
|
minLength: 1
|
||||||
|
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
description: status of the condition, one of True, False, Unknown.
|
||||||
|
enum:
|
||||||
|
- "True"
|
||||||
|
- "False"
|
||||||
|
- Unknown
|
||||||
|
type: string
|
||||||
|
type:
|
||||||
|
description: type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||||
|
maxLength: 316
|
||||||
|
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- lastTransitionTime
|
||||||
|
- message
|
||||||
|
- reason
|
||||||
|
- status
|
||||||
|
- type
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-map-keys:
|
||||||
|
- type
|
||||||
|
x-kubernetes-list-type: map
|
||||||
|
observedGeneration:
|
||||||
|
format: int64
|
||||||
|
type: integer
|
||||||
|
phase:
|
||||||
|
description: Phase is a coarse lifecycle summary (Pending/Ready/Error).
|
||||||
|
type: string
|
||||||
|
recordCount:
|
||||||
|
description: RecordCount is the number of managed record sets applied.
|
||||||
|
format: int32
|
||||||
|
type: integer
|
||||||
|
serial:
|
||||||
|
description: Serial is the last observed SOA serial on the primary.
|
||||||
|
format: int64
|
||||||
|
type: integer
|
||||||
|
signed:
|
||||||
|
description: Signed reports whether DNSSEC signing is active.
|
||||||
|
type: boolean
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
served: true
|
||||||
|
storage: true
|
||||||
|
subresources:
|
||||||
|
status: {}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
---
|
||||||
|
apiVersion: apiextensions.k8s.io/v1
|
||||||
|
kind: CustomResourceDefinition
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
controller-gen.kubebuilder.io/version: v0.17.3
|
||||||
|
name: dnsrecords.bind.unkin.net
|
||||||
|
spec:
|
||||||
|
group: bind.unkin.net
|
||||||
|
names:
|
||||||
|
kind: DNSRecord
|
||||||
|
listKind: DNSRecordList
|
||||||
|
plural: dnsrecords
|
||||||
|
shortNames:
|
||||||
|
- dnsr
|
||||||
|
singular: dnsrecord
|
||||||
|
scope: Namespaced
|
||||||
|
versions:
|
||||||
|
- additionalPrinterColumns:
|
||||||
|
- jsonPath: .spec.zoneRef
|
||||||
|
name: Zone
|
||||||
|
type: string
|
||||||
|
- jsonPath: .spec.name
|
||||||
|
name: Name
|
||||||
|
type: string
|
||||||
|
- jsonPath: .spec.type
|
||||||
|
name: Type
|
||||||
|
type: string
|
||||||
|
- jsonPath: .status.phase
|
||||||
|
name: Phase
|
||||||
|
type: string
|
||||||
|
name: v1alpha1
|
||||||
|
schema:
|
||||||
|
openAPIV3Schema:
|
||||||
|
description: DNSRecord is an individually-managed record set applied to a
|
||||||
|
BindZone.
|
||||||
|
properties:
|
||||||
|
apiVersion:
|
||||||
|
description: |-
|
||||||
|
APIVersion defines the versioned schema of this representation of an object.
|
||||||
|
Servers should convert recognized schemas to the latest internal value, and
|
||||||
|
may reject unrecognized values.
|
||||||
|
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
|
||||||
|
type: string
|
||||||
|
kind:
|
||||||
|
description: |-
|
||||||
|
Kind is a string value representing the REST resource this object represents.
|
||||||
|
Servers may infer this from the endpoint the client submits requests to.
|
||||||
|
Cannot be updated.
|
||||||
|
In CamelCase.
|
||||||
|
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
|
||||||
|
type: string
|
||||||
|
metadata:
|
||||||
|
type: object
|
||||||
|
spec:
|
||||||
|
description: |-
|
||||||
|
DNSRecordSpec defines a single record set applied to a zone via TSIG dynamic
|
||||||
|
update (nsupdate) — the external-dns write path expressed as a CRD.
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
default: '@'
|
||||||
|
description: Name is the owner name, relative to the zone apex or
|
||||||
|
fully qualified.
|
||||||
|
type: string
|
||||||
|
ttl:
|
||||||
|
description: TTL for the record set in seconds. Falls back to the
|
||||||
|
zone default TTL.
|
||||||
|
format: int32
|
||||||
|
type: integer
|
||||||
|
type:
|
||||||
|
description: Type is the RR type, e.g. A, AAAA, CNAME, TXT, SRV, MX.
|
||||||
|
type: string
|
||||||
|
values:
|
||||||
|
description: Values are the RDATA entries.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
minItems: 1
|
||||||
|
type: array
|
||||||
|
zoneRef:
|
||||||
|
description: |-
|
||||||
|
ZoneRef names the BindZone this record belongs to. The cluster, view and
|
||||||
|
update key are derived from the referenced zone.
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- type
|
||||||
|
- values
|
||||||
|
- zoneRef
|
||||||
|
type: object
|
||||||
|
status:
|
||||||
|
description: DNSRecordStatus reports observed record state.
|
||||||
|
properties:
|
||||||
|
conditions:
|
||||||
|
items:
|
||||||
|
description: Condition contains details for one aspect of the current
|
||||||
|
state of this API Resource.
|
||||||
|
properties:
|
||||||
|
lastTransitionTime:
|
||||||
|
description: |-
|
||||||
|
lastTransitionTime is the last time the condition transitioned from one status to another.
|
||||||
|
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
|
||||||
|
format: date-time
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
description: |-
|
||||||
|
message is a human readable message indicating details about the transition.
|
||||||
|
This may be an empty string.
|
||||||
|
maxLength: 32768
|
||||||
|
type: string
|
||||||
|
observedGeneration:
|
||||||
|
description: |-
|
||||||
|
observedGeneration represents the .metadata.generation that the condition was set based upon.
|
||||||
|
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
|
||||||
|
with respect to the current state of the instance.
|
||||||
|
format: int64
|
||||||
|
minimum: 0
|
||||||
|
type: integer
|
||||||
|
reason:
|
||||||
|
description: |-
|
||||||
|
reason contains a programmatic identifier indicating the reason for the condition's last transition.
|
||||||
|
Producers of specific condition types may define expected values and meanings for this field,
|
||||||
|
and whether the values are considered a guaranteed API.
|
||||||
|
The value should be a CamelCase string.
|
||||||
|
This field may not be empty.
|
||||||
|
maxLength: 1024
|
||||||
|
minLength: 1
|
||||||
|
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
description: status of the condition, one of True, False, Unknown.
|
||||||
|
enum:
|
||||||
|
- "True"
|
||||||
|
- "False"
|
||||||
|
- Unknown
|
||||||
|
type: string
|
||||||
|
type:
|
||||||
|
description: type of condition in CamelCase or in foo.example.com/CamelCase.
|
||||||
|
maxLength: 316
|
||||||
|
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- lastTransitionTime
|
||||||
|
- message
|
||||||
|
- reason
|
||||||
|
- status
|
||||||
|
- type
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-map-keys:
|
||||||
|
- type
|
||||||
|
x-kubernetes-list-type: map
|
||||||
|
fqdn:
|
||||||
|
description: FQDN is the fully-qualified owner name that was applied.
|
||||||
|
type: string
|
||||||
|
observedGeneration:
|
||||||
|
format: int64
|
||||||
|
type: integer
|
||||||
|
phase:
|
||||||
|
description: Phase is a coarse lifecycle summary (Pending/Applied/Error).
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
served: true
|
||||||
|
storage: true
|
||||||
|
subresources:
|
||||||
|
status: {}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: bind-operator
|
||||||
|
rules:
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- configmaps
|
||||||
|
- secrets
|
||||||
|
- services
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- pods
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- ""
|
||||||
|
resources:
|
||||||
|
- pods/exec
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- get
|
||||||
|
- apiGroups:
|
||||||
|
- apps
|
||||||
|
resources:
|
||||||
|
- statefulsets
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- bind.unkin.net
|
||||||
|
resources:
|
||||||
|
- bindacls
|
||||||
|
- bindcatalogzones
|
||||||
|
- bindclusters
|
||||||
|
- binddnssecpolicies
|
||||||
|
- bindpolicies
|
||||||
|
- bindtsigkeys
|
||||||
|
- bindviews
|
||||||
|
- bindzones
|
||||||
|
- dnsrecords
|
||||||
|
verbs:
|
||||||
|
- create
|
||||||
|
- delete
|
||||||
|
- get
|
||||||
|
- list
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
|
- watch
|
||||||
|
- apiGroups:
|
||||||
|
- bind.unkin.net
|
||||||
|
resources:
|
||||||
|
- bindacls/status
|
||||||
|
- bindcatalogzones/status
|
||||||
|
- bindclusters/status
|
||||||
|
- binddnssecpolicies/status
|
||||||
|
- bindpolicies/status
|
||||||
|
- bindtsigkeys/status
|
||||||
|
- bindviews/status
|
||||||
|
- bindzones/status
|
||||||
|
- dnsrecords/status
|
||||||
|
verbs:
|
||||||
|
- get
|
||||||
|
- patch
|
||||||
|
- update
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
# TSIG key used to authenticate zone transfers between primary and secondaries
|
||||||
|
# (and catalog zone transfers). The operator generates the material into a
|
||||||
|
# Secret named <name>-tsig; the key never appears in the CR.
|
||||||
|
apiVersion: bind.unkin.net/v1alpha1
|
||||||
|
kind: BindTSIGKey
|
||||||
|
metadata:
|
||||||
|
name: transfer-key
|
||||||
|
namespace: bind-auth
|
||||||
|
spec:
|
||||||
|
algorithm: hmac-sha256
|
||||||
|
---
|
||||||
|
# TSIG key permitting external-dns (and DNSRecord objects) to send RFC2136
|
||||||
|
# dynamic updates to the dynamic cluster's primary.
|
||||||
|
apiVersion: bind.unkin.net/v1alpha1
|
||||||
|
kind: BindTSIGKey
|
||||||
|
metadata:
|
||||||
|
name: externaldns-key
|
||||||
|
namespace: bind-externaldns
|
||||||
|
spec:
|
||||||
|
algorithm: hmac-sha256
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
---
|
||||||
|
# Authoritative masters role (replaces 3x Puppet authoritative servers).
|
||||||
|
# Ordinal-0 is the primary holding zone data; the other two replicate via the
|
||||||
|
# catalog zone + AXFR/IXFR.
|
||||||
|
apiVersion: bind.unkin.net/v1alpha1
|
||||||
|
kind: BindCluster
|
||||||
|
metadata:
|
||||||
|
name: auth
|
||||||
|
namespace: bind-auth
|
||||||
|
spec:
|
||||||
|
mode: authoritative
|
||||||
|
replicas: 3
|
||||||
|
storageSize: 2Gi
|
||||||
|
service:
|
||||||
|
type: LoadBalancer
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 128Mi
|
||||||
|
limits:
|
||||||
|
cpu: "1"
|
||||||
|
memory: 512Mi
|
||||||
|
---
|
||||||
|
apiVersion: bind.unkin.net/v1alpha1
|
||||||
|
kind: BindACL
|
||||||
|
metadata:
|
||||||
|
name: internal-nets
|
||||||
|
namespace: bind-auth
|
||||||
|
spec:
|
||||||
|
clusterRef: auth
|
||||||
|
entries:
|
||||||
|
- 10.0.0.0/8
|
||||||
|
- 192.168.0.0/16
|
||||||
|
---
|
||||||
|
# Catalog zone: new BindZones are auto-provisioned onto the secondaries.
|
||||||
|
apiVersion: bind.unkin.net/v1alpha1
|
||||||
|
kind: BindCatalogZone
|
||||||
|
metadata:
|
||||||
|
name: auth-catalog
|
||||||
|
namespace: bind-auth
|
||||||
|
spec:
|
||||||
|
clusterRef: auth
|
||||||
|
zoneName: catalog.internal
|
||||||
|
transferKeyRef: transfer-key
|
||||||
|
---
|
||||||
|
apiVersion: bind.unkin.net/v1alpha1
|
||||||
|
kind: BindDNSSECPolicy
|
||||||
|
metadata:
|
||||||
|
name: standard
|
||||||
|
namespace: bind-auth
|
||||||
|
spec:
|
||||||
|
clusterRef: auth
|
||||||
|
algorithm: ecdsap256sha256
|
||||||
|
nsec3: true
|
||||||
|
csk:
|
||||||
|
lifetime: unlimited
|
||||||
|
---
|
||||||
|
# Forward zone (signed) with a couple of seeded records.
|
||||||
|
apiVersion: bind.unkin.net/v1alpha1
|
||||||
|
kind: BindZone
|
||||||
|
metadata:
|
||||||
|
name: example-internal
|
||||||
|
namespace: bind-auth
|
||||||
|
spec:
|
||||||
|
clusterRef: auth
|
||||||
|
zoneName: internal.example.com
|
||||||
|
type: primary
|
||||||
|
defaultTTL: 3600
|
||||||
|
dnssecPolicyRef: standard
|
||||||
|
allowTransfer:
|
||||||
|
- key transfer-key
|
||||||
|
updateKeyRef: transfer-key
|
||||||
|
dynamicUpdate: true
|
||||||
|
records:
|
||||||
|
- name: "@"
|
||||||
|
type: NS
|
||||||
|
values: ["ns1.internal.example.com."]
|
||||||
|
- name: ns1
|
||||||
|
type: A
|
||||||
|
values: ["10.0.0.53"]
|
||||||
|
- name: www
|
||||||
|
type: A
|
||||||
|
values: ["10.0.1.10", "10.0.1.11"]
|
||||||
|
---
|
||||||
|
# Reverse zone for 10.0.0.0/16.
|
||||||
|
apiVersion: bind.unkin.net/v1alpha1
|
||||||
|
kind: BindZone
|
||||||
|
metadata:
|
||||||
|
name: reverse-10-0
|
||||||
|
namespace: bind-auth
|
||||||
|
spec:
|
||||||
|
clusterRef: auth
|
||||||
|
zoneName: 0.10.in-addr.arpa
|
||||||
|
type: primary
|
||||||
|
updateKeyRef: transfer-key
|
||||||
|
dynamicUpdate: true
|
||||||
|
allowTransfer:
|
||||||
|
- key transfer-key
|
||||||
|
records:
|
||||||
|
- name: "53.0"
|
||||||
|
type: PTR
|
||||||
|
values: ["ns1.internal.example.com."]
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
# Recursive resolvers role (replaces 3x Puppet only-resolver servers).
|
||||||
|
# All three pods are identical recursive servers; no zone replication.
|
||||||
|
apiVersion: bind.unkin.net/v1alpha1
|
||||||
|
kind: BindCluster
|
||||||
|
metadata:
|
||||||
|
name: resolver
|
||||||
|
namespace: bind-resolver
|
||||||
|
spec:
|
||||||
|
mode: resolver
|
||||||
|
replicas: 3
|
||||||
|
service:
|
||||||
|
type: LoadBalancer
|
||||||
|
forwarders:
|
||||||
|
- 1.1.1.1
|
||||||
|
- 9.9.9.9
|
||||||
|
---
|
||||||
|
# Conditional forwarding of an internal zone to the authoritative cluster.
|
||||||
|
apiVersion: bind.unkin.net/v1alpha1
|
||||||
|
kind: BindZone
|
||||||
|
metadata:
|
||||||
|
name: forward-internal
|
||||||
|
namespace: bind-resolver
|
||||||
|
spec:
|
||||||
|
clusterRef: resolver
|
||||||
|
zoneName: internal.example.com
|
||||||
|
type: forward
|
||||||
|
catalog: false
|
||||||
|
forwarders:
|
||||||
|
- 10.0.0.53
|
||||||
|
---
|
||||||
|
# DNS firewall (RPZ) blocklist applied to the resolvers.
|
||||||
|
apiVersion: bind.unkin.net/v1alpha1
|
||||||
|
kind: BindPolicy
|
||||||
|
metadata:
|
||||||
|
name: blocklist
|
||||||
|
namespace: bind-resolver
|
||||||
|
spec:
|
||||||
|
clusterRef: resolver
|
||||||
|
zoneName: rpz.internal
|
||||||
|
order: 10
|
||||||
|
transferKeyRef: transfer-key
|
||||||
|
rules:
|
||||||
|
- trigger: qname
|
||||||
|
match: malware.example.
|
||||||
|
action: nxdomain
|
||||||
|
- trigger: qname
|
||||||
|
match: tracker.example.
|
||||||
|
action: cname
|
||||||
|
target: blocked.internal.example.com
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
# external-dns role (replaces 3x Puppet external-dns servers). The primary
|
||||||
|
# accepts RFC2136 TSIG updates from external-dns; secondaries replicate.
|
||||||
|
apiVersion: bind.unkin.net/v1alpha1
|
||||||
|
kind: BindCluster
|
||||||
|
metadata:
|
||||||
|
name: externaldns
|
||||||
|
namespace: bind-externaldns
|
||||||
|
spec:
|
||||||
|
mode: dynamic
|
||||||
|
replicas: 3
|
||||||
|
service:
|
||||||
|
type: LoadBalancer
|
||||||
|
---
|
||||||
|
# Public zone that external-dns writes into via nsupdate/TSIG.
|
||||||
|
apiVersion: bind.unkin.net/v1alpha1
|
||||||
|
kind: BindZone
|
||||||
|
metadata:
|
||||||
|
name: example-com
|
||||||
|
namespace: bind-externaldns
|
||||||
|
spec:
|
||||||
|
clusterRef: externaldns
|
||||||
|
zoneName: example.com
|
||||||
|
type: primary
|
||||||
|
dynamicUpdate: true
|
||||||
|
updateKeyRef: externaldns-key
|
||||||
|
allowTransfer:
|
||||||
|
- key externaldns-key
|
||||||
|
---
|
||||||
|
# A record managed as a CRD (external-dns-style) instead of via the RFC2136
|
||||||
|
# controller — same write path (TSIG nsupdate to the primary).
|
||||||
|
apiVersion: bind.unkin.net/v1alpha1
|
||||||
|
kind: DNSRecord
|
||||||
|
metadata:
|
||||||
|
name: www-example-com
|
||||||
|
namespace: bind-externaldns
|
||||||
|
spec:
|
||||||
|
zoneRef: example-com
|
||||||
|
name: www
|
||||||
|
type: A
|
||||||
|
ttl: 300
|
||||||
|
values:
|
||||||
|
- 203.0.113.10
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
# Split-horizon example: an internal view answering RFC1918 clients and a
|
||||||
|
# default external view. Bind a zone to a view via BindZone.spec.viewRef.
|
||||||
|
apiVersion: bind.unkin.net/v1alpha1
|
||||||
|
kind: BindView
|
||||||
|
metadata:
|
||||||
|
name: internal
|
||||||
|
namespace: bind-auth
|
||||||
|
spec:
|
||||||
|
clusterRef: auth
|
||||||
|
order: 10
|
||||||
|
matchClients:
|
||||||
|
- internal-nets
|
||||||
|
recursion: false
|
||||||
|
---
|
||||||
|
apiVersion: bind.unkin.net/v1alpha1
|
||||||
|
kind: BindView
|
||||||
|
metadata:
|
||||||
|
name: external
|
||||||
|
namespace: bind-auth
|
||||||
|
spec:
|
||||||
|
clusterRef: auth
|
||||||
|
order: 100
|
||||||
|
matchClients:
|
||||||
|
- any
|
||||||
|
recursion: false
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
module git.unkin.net/unkin/bind-operator
|
||||||
|
|
||||||
|
go 1.25
|
||||||
|
|
||||||
|
require (
|
||||||
|
k8s.io/api v0.34.4
|
||||||
|
k8s.io/apimachinery v0.34.4
|
||||||
|
k8s.io/client-go v0.34.4
|
||||||
|
sigs.k8s.io/controller-runtime v0.22.4
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
|
||||||
|
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
|
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
|
github.com/go-logr/zapr v1.3.0 // indirect
|
||||||
|
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||||
|
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||||
|
github.com/go-openapi/swag v0.23.0 // indirect
|
||||||
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
|
github.com/google/btree v1.1.3 // indirect
|
||||||
|
github.com/google/gnostic-models v0.7.0 // indirect
|
||||||
|
github.com/google/go-cmp v0.7.0 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
|
||||||
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
|
github.com/moby/spdystream v0.5.0 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
|
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/prometheus/client_golang v1.22.0 // indirect
|
||||||
|
github.com/prometheus/client_model v0.6.1 // indirect
|
||||||
|
github.com/prometheus/common v0.62.0 // indirect
|
||||||
|
github.com/prometheus/procfs v0.15.1 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.6 // indirect
|
||||||
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
|
go.uber.org/zap v1.27.0 // indirect
|
||||||
|
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
|
golang.org/x/net v0.38.0 // indirect
|
||||||
|
golang.org/x/oauth2 v0.27.0 // indirect
|
||||||
|
golang.org/x/sync v0.12.0 // indirect
|
||||||
|
golang.org/x/sys v0.31.0 // indirect
|
||||||
|
golang.org/x/term v0.30.0 // indirect
|
||||||
|
golang.org/x/text v0.23.0 // indirect
|
||||||
|
golang.org/x/time v0.9.0 // indirect
|
||||||
|
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.5 // indirect
|
||||||
|
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||||
|
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
k8s.io/apiextensions-apiserver v0.34.1 // indirect
|
||||||
|
k8s.io/klog/v2 v2.130.1 // indirect
|
||||||
|
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
|
||||||
|
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
|
||||||
|
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
|
||||||
|
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||||
|
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
|
||||||
|
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||||
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||||
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
|
||||||
|
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||||
|
github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
|
||||||
|
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
|
||||||
|
github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
|
||||||
|
github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
|
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||||
|
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||||
|
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
|
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
|
||||||
|
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
|
||||||
|
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
||||||
|
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||||
|
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||||
|
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
|
||||||
|
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
|
||||||
|
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||||
|
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||||
|
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||||
|
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||||
|
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||||
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
|
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||||
|
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||||
|
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
|
||||||
|
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||||
|
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
|
||||||
|
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
|
||||||
|
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
|
||||||
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||||
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
|
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||||
|
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
|
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
|
||||||
|
github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
|
||||||
|
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
|
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
|
||||||
|
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||||
|
github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg=
|
||||||
|
github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
|
||||||
|
github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw=
|
||||||
|
github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||||
|
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||||
|
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||||
|
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||||
|
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
||||||
|
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||||
|
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||||
|
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||||
|
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
|
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||||
|
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||||
|
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
|
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||||
|
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
|
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||||
|
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||||
|
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||||
|
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
|
||||||
|
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||||
|
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||||
|
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
||||||
|
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||||
|
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||||
|
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||||
|
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
|
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
||||||
|
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
|
||||||
|
gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
|
||||||
|
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||||
|
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
|
||||||
|
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||||
|
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||||
|
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
k8s.io/api v0.34.4 h1:Z5hsoQcZ2yBjelb9j5JKzCVo9qv9XLkVm5llnqS4h+0=
|
||||||
|
k8s.io/api v0.34.4/go.mod h1:6SaGYuGPkMqqCgg8rPG/OQoCrhgSEV+wWn9v21fDP3o=
|
||||||
|
k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI=
|
||||||
|
k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc=
|
||||||
|
k8s.io/apimachinery v0.34.4 h1:C5SiSzLEMyWIk53sSbnk0WlOOyqv/MFnWvuc/d6M+xc=
|
||||||
|
k8s.io/apimachinery v0.34.4/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
|
||||||
|
k8s.io/client-go v0.34.4 h1:IXhvzFdm0e897kXtLbeyMpAGzontcShJ/gi/XCCsOLc=
|
||||||
|
k8s.io/client-go v0.34.4/go.mod h1:tXIVJTQabT5QRGlFdxZQFxrIhcGUPpKL5DAc4gSWTE8=
|
||||||
|
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||||
|
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||||
|
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
|
||||||
|
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
|
||||||
|
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
|
||||||
|
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||||
|
sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A=
|
||||||
|
sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
|
||||||
|
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
|
||||||
|
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||||
|
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||||
|
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||||
|
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
|
||||||
|
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||||
|
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||||
|
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: bind-operator-system
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: bind-operator
|
||||||
|
namespace: bind-operator-system
|
||||||
|
labels:
|
||||||
|
app: bind-operator
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: bind-operator
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: bind-operator
|
||||||
|
spec:
|
||||||
|
serviceAccountName: bind-operator
|
||||||
|
containers:
|
||||||
|
- name: operator
|
||||||
|
image: bind-operator:dev
|
||||||
|
imagePullPolicy: Never
|
||||||
|
args:
|
||||||
|
- --metrics-bind-address=:8080
|
||||||
|
- --health-probe-bind-address=:8081
|
||||||
|
- --leader-elect
|
||||||
|
ports:
|
||||||
|
- containerPort: 8080
|
||||||
|
name: metrics
|
||||||
|
- containerPort: 8081
|
||||||
|
name: health
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /readyz
|
||||||
|
port: 8081
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /healthz
|
||||||
|
port: 8081
|
||||||
|
initialDelaySeconds: 15
|
||||||
|
periodSeconds: 20
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 64Mi
|
||||||
|
limits:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 256Mi
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: bind-operator
|
||||||
|
namespace: bind-operator-system
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: bind-operator
|
||||||
|
rules:
|
||||||
|
- apiGroups: ["bind.unkin.net"]
|
||||||
|
resources: ["*"]
|
||||||
|
verbs: ["*"]
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["services", "configmaps", "secrets"]
|
||||||
|
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["pods"]
|
||||||
|
verbs: ["get", "list", "watch"]
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["pods/exec"]
|
||||||
|
verbs: ["create", "get"]
|
||||||
|
- apiGroups: ["apps"]
|
||||||
|
resources: ["statefulsets"]
|
||||||
|
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: ["events"]
|
||||||
|
verbs: ["create", "patch"]
|
||||||
|
- apiGroups: ["coordination.k8s.io"]
|
||||||
|
resources: ["leases"]
|
||||||
|
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
metadata:
|
||||||
|
name: bind-operator
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: bind-operator
|
||||||
|
namespace: bind-operator-system
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRole
|
||||||
|
name: bind-operator
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
// Package bind contains helpers for driving BIND9 pods: executing rndc and
|
||||||
|
// nsupdate over the Kubernetes exec subresource, and rendering named.conf.
|
||||||
|
package bind
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
|
"k8s.io/client-go/kubernetes/scheme"
|
||||||
|
"k8s.io/client-go/rest"
|
||||||
|
"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
|
||||||
|
clientset kubernetes.Interface
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewExecutor builds an Executor from a controller-runtime rest config.
|
||||||
|
func NewExecutor(cfg *rest.Config) (*Executor, error) {
|
||||||
|
cs, err := kubernetes.NewForConfig(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("build clientset: %w", err)
|
||||||
|
}
|
||||||
|
return &Executor{config: cfg, clientset: cs}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exec runs command in the BIND container of pod, optionally feeding stdin, and
|
||||||
|
// returns stdout. A non-zero exit or transport error yields an error that
|
||||||
|
// includes stderr.
|
||||||
|
func (e *Executor) Exec(ctx context.Context, namespace, pod string, command []string, stdin string) (string, error) {
|
||||||
|
req := e.clientset.CoreV1().RESTClient().Post().
|
||||||
|
Resource("pods").
|
||||||
|
Name(pod).
|
||||||
|
Namespace(namespace).
|
||||||
|
SubResource("exec").
|
||||||
|
VersionedParams(&corev1.PodExecOptions{
|
||||||
|
Container: ContainerName,
|
||||||
|
Command: command,
|
||||||
|
Stdin: stdin != "",
|
||||||
|
Stdout: true,
|
||||||
|
Stderr: true,
|
||||||
|
}, scheme.ParameterCodec)
|
||||||
|
|
||||||
|
exec, err := remotecommand.NewSPDYExecutor(e.config, "POST", req.URL())
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("spdy executor: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
opts := remotecommand.StreamOptions{Stdout: &stdout, Stderr: &stderr}
|
||||||
|
if stdin != "" {
|
||||||
|
opts.Stdin = bytes.NewBufferString(stdin)
|
||||||
|
}
|
||||||
|
if err := exec.StreamWithContext(ctx, opts); err != nil {
|
||||||
|
return stdout.String(), fmt.Errorf("exec %v: %w (stderr: %s)", command, err, stderr.String())
|
||||||
|
}
|
||||||
|
return stdout.String(), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package bind
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/hex"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// catalogHash returns the unique member label for a catalog zone entry: the
|
||||||
|
// hex-encoded SHA-1 digest of the member zone name in DNS wire format, per the
|
||||||
|
// BIND catalog-zone schema (RFC 9432).
|
||||||
|
func catalogHash(zone string) string {
|
||||||
|
sum := sha1.Sum(wireName(zone))
|
||||||
|
return hex.EncodeToString(sum[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// wireName encodes a domain name into uncompressed DNS wire format: each label
|
||||||
|
// length-prefixed, terminated by a zero-length root label. Names are lowercased.
|
||||||
|
func wireName(name string) []byte {
|
||||||
|
name = strings.TrimSuffix(strings.ToLower(strings.TrimSpace(name)), ".")
|
||||||
|
var out []byte
|
||||||
|
if name != "" {
|
||||||
|
for _, label := range strings.Split(name, ".") {
|
||||||
|
out = append(out, byte(len(label)))
|
||||||
|
out = append(out, []byte(label)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return append(out, 0)
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package bind
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenerateSecret returns a base64-encoded cryptographically-random key of n
|
||||||
|
// bytes, suitable for a TSIG or rndc HMAC secret.
|
||||||
|
func GenerateSecret(n int) (string, error) {
|
||||||
|
buf := make([]byte, n)
|
||||||
|
if _, err := rand.Read(buf); err != nil {
|
||||||
|
return "", fmt.Errorf("read random: %w", err)
|
||||||
|
}
|
||||||
|
return base64.StdEncoding.EncodeToString(buf), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyClause renders a named.conf `key` block for inclusion.
|
||||||
|
func KeyClause(name, algorithm, secret string) string {
|
||||||
|
return fmt.Sprintf("key \"%s\" {\n algorithm %s;\n secret \"%s\";\n};\n", name, algorithm, secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SecretBytesForAlgorithm returns a reasonable key length for a TSIG algorithm.
|
||||||
|
func SecretBytesForAlgorithm(algorithm string) int {
|
||||||
|
switch algorithm {
|
||||||
|
case "hmac-sha512", "hmac-sha384":
|
||||||
|
return 64
|
||||||
|
case "hmac-sha256":
|
||||||
|
return 32
|
||||||
|
default:
|
||||||
|
return 32
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package bind
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TSIGCreds carries the material needed to authenticate a dynamic update.
|
||||||
|
type TSIGCreds struct {
|
||||||
|
Name string // TSIG key name
|
||||||
|
Algorithm string // e.g. hmac-sha256
|
||||||
|
Secret string // base64-encoded key
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordUpdate describes a desired record set to apply to a zone.
|
||||||
|
type RecordUpdate struct {
|
||||||
|
FQDN string // fully-qualified owner name, trailing dot recommended
|
||||||
|
Type string // RR type
|
||||||
|
TTL int32 // record TTL
|
||||||
|
Values []string // RDATA entries
|
||||||
|
Delete bool // when true, delete the RRset instead of replacing it
|
||||||
|
}
|
||||||
|
|
||||||
|
// NSUpdate applies a set of record changes to zone by executing nsupdate on the
|
||||||
|
// primary pod, targeting the local server and authenticating with creds. All
|
||||||
|
// changes are sent in a single atomic transaction.
|
||||||
|
func (e *Executor) NSUpdate(ctx context.Context, namespace, pod, zone string, creds TSIGCreds, updates []RecordUpdate) error {
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString("server 127.0.0.1\n")
|
||||||
|
b.WriteString(fmt.Sprintf("zone %s\n", dot(zone)))
|
||||||
|
for _, u := range updates {
|
||||||
|
// Replace semantics: clear the RRset first, then add the desired values.
|
||||||
|
b.WriteString(fmt.Sprintf("update delete %s %s\n", dot(u.FQDN), u.Type))
|
||||||
|
if u.Delete {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, v := range u.Values {
|
||||||
|
b.WriteString(fmt.Sprintf("update add %s %d %s %s\n", dot(u.FQDN), u.TTL, u.Type, v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.WriteString("send\n")
|
||||||
|
|
||||||
|
cmd := []string{"nsupdate", "-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)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// dot ensures a name is fully qualified with a trailing dot.
|
||||||
|
func dot(name string) string {
|
||||||
|
if name == "" || name == "@" {
|
||||||
|
return "@"
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(name, ".") {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
return name + "."
|
||||||
|
}
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
package bind
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
bindv1alpha1 "git.unkin.net/unkin/bind-operator/api/v1alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RenderInput aggregates everything needed to render a cluster's named.conf.
|
||||||
|
type RenderInput struct {
|
||||||
|
Cluster *bindv1alpha1.BindCluster
|
||||||
|
ACLs []bindv1alpha1.BindACL
|
||||||
|
Views []bindv1alpha1.BindView
|
||||||
|
Policies []bindv1alpha1.BindPolicy
|
||||||
|
DNSSECPolicies []bindv1alpha1.BindDNSSECPolicy
|
||||||
|
Catalog *bindv1alpha1.BindCatalogZone
|
||||||
|
// PrimaryAddress is the in-cluster address secondaries transfer from.
|
||||||
|
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.
|
||||||
|
func RenderNamedConf(in RenderInput) (primary string, secondary string) {
|
||||||
|
return render(in, true), render(in, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func render(in RenderInput, isPrimary bool) string {
|
||||||
|
c := in.Cluster
|
||||||
|
var b strings.Builder
|
||||||
|
|
||||||
|
b.WriteString("// Managed by bind-operator. Do not edit.\n")
|
||||||
|
b.WriteString(`include "/etc/bind/keys/keys.conf";` + "\n\n")
|
||||||
|
|
||||||
|
// Named ACLs (global scope).
|
||||||
|
acls := append([]bindv1alpha1.BindACL(nil), in.ACLs...)
|
||||||
|
sort.Slice(acls, func(i, j int) bool { return acls[i].Name < acls[j].Name })
|
||||||
|
for _, a := range acls {
|
||||||
|
b.WriteString(fmt.Sprintf("acl \"%s\" { %s };\n", a.Name, matchList(a.Spec.Entries)))
|
||||||
|
}
|
||||||
|
if len(acls) > 0 {
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNSSEC policies (must precede zones that reference them).
|
||||||
|
for _, p := range in.DNSSECPolicies {
|
||||||
|
b.WriteString(renderDNSSECPolicy(p))
|
||||||
|
}
|
||||||
|
|
||||||
|
// options.
|
||||||
|
b.WriteString("options {\n")
|
||||||
|
b.WriteString(fmt.Sprintf(" directory \"%s\";\n", DataDir))
|
||||||
|
b.WriteString(" listen-on port 53 { any; };\n")
|
||||||
|
b.WriteString(" listen-on-v6 port 53 { any; };\n")
|
||||||
|
b.WriteString(fmt.Sprintf(" recursion %s;\n", yesno(recursionFor(c))))
|
||||||
|
if len(c.Spec.Forwarders) > 0 {
|
||||||
|
b.WriteString(fmt.Sprintf(" forwarders { %s };\n", terminate(c.Spec.Forwarders)))
|
||||||
|
}
|
||||||
|
if allowNewZones(c) {
|
||||||
|
b.WriteString(" allow-new-zones yes;\n")
|
||||||
|
}
|
||||||
|
b.WriteString(" dnssec-validation auto;\n")
|
||||||
|
for _, o := range c.Spec.ExtraOptions {
|
||||||
|
b.WriteString(" " + strings.TrimRight(o, ";") + ";\n")
|
||||||
|
}
|
||||||
|
// When there are no views, response-policy and catalog-zones live in options.
|
||||||
|
if len(in.Views) == 0 {
|
||||||
|
b.WriteString(responsePolicyClause(in.Policies, " "))
|
||||||
|
b.WriteString(catalogZonesClause(in, isPrimary, " "))
|
||||||
|
}
|
||||||
|
b.WriteString("};\n\n")
|
||||||
|
|
||||||
|
// controls (rndc).
|
||||||
|
b.WriteString("controls {\n")
|
||||||
|
b.WriteString(" inet 127.0.0.1 port 953 allow { 127.0.0.1; } keys { \"rndc-key\"; };\n")
|
||||||
|
b.WriteString("};\n\n")
|
||||||
|
|
||||||
|
// Views, if any.
|
||||||
|
views := append([]bindv1alpha1.BindView(nil), in.Views...)
|
||||||
|
sort.Slice(views, func(i, j int) bool { return views[i].Spec.Order < views[j].Spec.Order })
|
||||||
|
for _, v := range views {
|
||||||
|
b.WriteString(renderView(v, in, isPrimary))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Catalog zone declaration lives at top level when there are no views.
|
||||||
|
if in.Catalog != nil && len(in.Views) == 0 {
|
||||||
|
b.WriteString(renderCatalogZoneDecl(in, isPrimary, ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderView(v bindv1alpha1.BindView, in RenderInput, isPrimary bool) string {
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString(fmt.Sprintf("view \"%s\" {\n", v.Name))
|
||||||
|
mc := v.Spec.MatchClients
|
||||||
|
if len(mc) == 0 {
|
||||||
|
mc = []string{"any"}
|
||||||
|
}
|
||||||
|
b.WriteString(fmt.Sprintf(" match-clients { %s };\n", matchList(mc)))
|
||||||
|
if len(v.Spec.MatchDestinations) > 0 {
|
||||||
|
b.WriteString(fmt.Sprintf(" match-destinations { %s };\n", matchList(v.Spec.MatchDestinations)))
|
||||||
|
}
|
||||||
|
rec := recursionFor(in.Cluster)
|
||||||
|
if v.Spec.Recursion != nil {
|
||||||
|
rec = *v.Spec.Recursion
|
||||||
|
}
|
||||||
|
b.WriteString(fmt.Sprintf(" recursion %s;\n", yesno(rec)))
|
||||||
|
if len(v.Spec.AllowQuery) > 0 {
|
||||||
|
b.WriteString(fmt.Sprintf(" allow-query { %s };\n", matchList(v.Spec.AllowQuery)))
|
||||||
|
}
|
||||||
|
for _, o := range v.Spec.ExtraOptions {
|
||||||
|
b.WriteString(" " + strings.TrimRight(o, ";") + ";\n")
|
||||||
|
}
|
||||||
|
// Policies and catalog scoped to this view.
|
||||||
|
viewPolicies := filterPoliciesForView(in.Policies, v.Name)
|
||||||
|
b.WriteString(responsePolicyClause(viewPolicies, " "))
|
||||||
|
b.WriteString(catalogZonesClause(in, isPrimary, " "))
|
||||||
|
if in.Catalog != nil {
|
||||||
|
b.WriteString(renderCatalogZoneDecl(in, isPrimary, " "))
|
||||||
|
}
|
||||||
|
b.WriteString("};\n\n")
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderDNSSECPolicy(p bindv1alpha1.BindDNSSECPolicy) string {
|
||||||
|
name := p.Spec.PolicyName
|
||||||
|
if name == "" {
|
||||||
|
name = p.Name
|
||||||
|
}
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString(fmt.Sprintf("dnssec-policy \"%s\" {\n", name))
|
||||||
|
if p.Spec.NSEC3 {
|
||||||
|
b.WriteString(" nsec3param;\n")
|
||||||
|
}
|
||||||
|
if p.Spec.MaxZoneTTL != "" {
|
||||||
|
b.WriteString(fmt.Sprintf(" max-zone-ttl %s;\n", p.Spec.MaxZoneTTL))
|
||||||
|
}
|
||||||
|
if p.Spec.SignaturesValidity != "" {
|
||||||
|
b.WriteString(fmt.Sprintf(" signatures-validity %s;\n", p.Spec.SignaturesValidity))
|
||||||
|
}
|
||||||
|
alg := p.Spec.Algorithm
|
||||||
|
if alg == "" {
|
||||||
|
alg = "ecdsap256sha256"
|
||||||
|
}
|
||||||
|
if p.Spec.CSK != nil {
|
||||||
|
b.WriteString(" keys {\n")
|
||||||
|
b.WriteString(" csk " + keyLine(p.Spec.CSK, alg) + ";\n")
|
||||||
|
b.WriteString(" };\n")
|
||||||
|
} else {
|
||||||
|
b.WriteString(" keys {\n")
|
||||||
|
if p.Spec.KSK != nil {
|
||||||
|
b.WriteString(" ksk " + keyLine(p.Spec.KSK, alg) + ";\n")
|
||||||
|
}
|
||||||
|
if p.Spec.ZSK != nil {
|
||||||
|
b.WriteString(" zsk " + keyLine(p.Spec.ZSK, alg) + ";\n")
|
||||||
|
}
|
||||||
|
b.WriteString(" };\n")
|
||||||
|
}
|
||||||
|
for _, o := range p.Spec.ExtraOptions {
|
||||||
|
b.WriteString(" " + strings.TrimRight(o, ";") + ";\n")
|
||||||
|
}
|
||||||
|
b.WriteString("};\n\n")
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func keyLine(k *bindv1alpha1.DNSSECKey, defaultAlg string) string {
|
||||||
|
lifetime := k.Lifetime
|
||||||
|
if lifetime == "" {
|
||||||
|
lifetime = "unlimited"
|
||||||
|
}
|
||||||
|
alg := k.Algorithm
|
||||||
|
if alg == "" {
|
||||||
|
alg = defaultAlg
|
||||||
|
}
|
||||||
|
if k.KeySize > 0 {
|
||||||
|
return fmt.Sprintf("lifetime %s algorithm %s %d", lifetime, alg, k.KeySize)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("lifetime %s algorithm %s", lifetime, alg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func responsePolicyClause(policies []bindv1alpha1.BindPolicy, indent string) string {
|
||||||
|
if len(policies) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
sorted := append([]bindv1alpha1.BindPolicy(nil), policies...)
|
||||||
|
sort.Slice(sorted, func(i, j int) bool { return sorted[i].Spec.Order < sorted[j].Spec.Order })
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString(indent + "response-policy {\n")
|
||||||
|
for _, p := range sorted {
|
||||||
|
b.WriteString(fmt.Sprintf("%s zone \"%s\";\n", indent, p.Spec.ZoneName))
|
||||||
|
}
|
||||||
|
b.WriteString(indent + "};\n")
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func catalogZonesClause(in RenderInput, isPrimary bool, indent string) string {
|
||||||
|
// Only secondaries consume the catalog to auto-provision member zones.
|
||||||
|
if in.Catalog == nil || isPrimary {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
primaries := in.Catalog.Spec.DefaultPrimaries
|
||||||
|
if len(primaries) == 0 && in.PrimaryAddress != "" {
|
||||||
|
primaries = []string{in.PrimaryAddress}
|
||||||
|
}
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString(indent + "catalog-zones {\n")
|
||||||
|
b.WriteString(fmt.Sprintf("%s zone \"%s\" default-primaries { %s };\n", indent, in.Catalog.Spec.ZoneName, terminate(primaries)))
|
||||||
|
b.WriteString(indent + "};\n")
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// renderCatalogZoneDecl declares the catalog zone as a secondary on consumer
|
||||||
|
// pods. The primary hosts the catalog zone dynamically (created by the
|
||||||
|
// BindCatalogZone controller via rndc addzone), so nothing is emitted here for
|
||||||
|
// the primary.
|
||||||
|
func renderCatalogZoneDecl(in RenderInput, isPrimary bool, indent string) string {
|
||||||
|
if isPrimary {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
cat := in.Catalog
|
||||||
|
file := CatalogFilePath(cat.Spec.ZoneName)
|
||||||
|
primaries := cat.Spec.DefaultPrimaries
|
||||||
|
if len(primaries) == 0 && in.PrimaryAddress != "" {
|
||||||
|
primaries = []string{in.PrimaryAddress}
|
||||||
|
}
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString(fmt.Sprintf("%szone \"%s\" {\n", indent, cat.Spec.ZoneName))
|
||||||
|
b.WriteString(indent + " type secondary;\n")
|
||||||
|
b.WriteString(fmt.Sprintf("%s file \"%s\";\n", indent, file))
|
||||||
|
b.WriteString(fmt.Sprintf("%s primaries { %s };\n", indent, terminate(primaries)))
|
||||||
|
b.WriteString(indent + "};\n\n")
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterPoliciesForView(policies []bindv1alpha1.BindPolicy, view string) []bindv1alpha1.BindPolicy {
|
||||||
|
var out []bindv1alpha1.BindPolicy
|
||||||
|
for _, p := range policies {
|
||||||
|
if p.Spec.ViewRef == view || p.Spec.ViewRef == "" {
|
||||||
|
out = append(out, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchList renders address-match-list elements, each terminated with a
|
||||||
|
// semicolon: `10.0.0.0/8; key foo;`.
|
||||||
|
func matchList(entries []string) string {
|
||||||
|
return terminate(entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
// terminate joins elements each followed by "; ".
|
||||||
|
func terminate(entries []string) string {
|
||||||
|
var parts []string
|
||||||
|
for _, e := range entries {
|
||||||
|
e = strings.TrimSpace(strings.TrimRight(e, ";"))
|
||||||
|
if e == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts = append(parts, e+";")
|
||||||
|
}
|
||||||
|
return strings.Join(parts, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func yesno(b bool) string {
|
||||||
|
if b {
|
||||||
|
return "yes"
|
||||||
|
}
|
||||||
|
return "no"
|
||||||
|
}
|
||||||
|
|
||||||
|
func recursionFor(c *bindv1alpha1.BindCluster) bool {
|
||||||
|
if c.Spec.Recursion != nil {
|
||||||
|
return *c.Spec.Recursion
|
||||||
|
}
|
||||||
|
return c.Spec.Mode == bindv1alpha1.ModeResolver
|
||||||
|
}
|
||||||
|
|
||||||
|
func allowNewZones(c *bindv1alpha1.BindCluster) bool {
|
||||||
|
if c.Spec.AllowNewZones != nil {
|
||||||
|
return *c.Spec.AllowNewZones
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package bind
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
bindv1alpha1 "git.unkin.net/unkin/bind-operator/api/v1alpha1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newCluster(mode bindv1alpha1.BindMode) *bindv1alpha1.BindCluster {
|
||||||
|
return &bindv1alpha1.BindCluster{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "auth", Namespace: "dns"},
|
||||||
|
Spec: bindv1alpha1.BindClusterSpec{Mode: mode, Replicas: 3},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenderResolverEnablesRecursion(t *testing.T) {
|
||||||
|
primary, secondary := RenderNamedConf(RenderInput{Cluster: newCluster(bindv1alpha1.ModeResolver)})
|
||||||
|
if !strings.Contains(primary, "recursion yes;") {
|
||||||
|
t.Fatalf("resolver primary should enable recursion:\n%s", primary)
|
||||||
|
}
|
||||||
|
if !strings.Contains(secondary, "recursion yes;") {
|
||||||
|
t.Fatalf("resolver secondary should enable recursion")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenderAuthoritativeDisablesRecursion(t *testing.T) {
|
||||||
|
primary, _ := RenderNamedConf(RenderInput{Cluster: newCluster(bindv1alpha1.ModeAuthoritative)})
|
||||||
|
if !strings.Contains(primary, "recursion no;") {
|
||||||
|
t.Fatalf("authoritative should disable recursion:\n%s", primary)
|
||||||
|
}
|
||||||
|
if !strings.Contains(primary, "allow-new-zones yes;") {
|
||||||
|
t.Fatalf("authoritative should allow new zones for dynamic provisioning")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenderCatalogOnSecondaryOnly(t *testing.T) {
|
||||||
|
in := RenderInput{
|
||||||
|
Cluster: newCluster(bindv1alpha1.ModeAuthoritative),
|
||||||
|
Catalog: &bindv1alpha1.BindCatalogZone{Spec: bindv1alpha1.BindCatalogZoneSpec{ZoneName: "catalog.internal", DefaultPrimaries: []string{"10.0.0.1"}}},
|
||||||
|
PrimaryAddress: "auth-0.auth-headless.dns.svc.cluster.local",
|
||||||
|
}
|
||||||
|
primary, secondary := RenderNamedConf(in)
|
||||||
|
if strings.Contains(primary, "catalog-zones") {
|
||||||
|
t.Fatalf("primary must not consume the catalog it publishes:\n%s", primary)
|
||||||
|
}
|
||||||
|
if !strings.Contains(secondary, "catalog-zones") {
|
||||||
|
t.Fatalf("secondary must consume the catalog zone:\n%s", secondary)
|
||||||
|
}
|
||||||
|
if !strings.Contains(secondary, "type secondary;") {
|
||||||
|
t.Fatalf("secondary must declare the catalog zone as a secondary")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRenderACL(t *testing.T) {
|
||||||
|
in := RenderInput{
|
||||||
|
Cluster: newCluster(bindv1alpha1.ModeAuthoritative),
|
||||||
|
ACLs: []bindv1alpha1.BindACL{{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "internal"},
|
||||||
|
Spec: bindv1alpha1.BindACLSpec{Entries: []string{"10.0.0.0/8", "192.168.0.0/16"}},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
primary, _ := RenderNamedConf(in)
|
||||||
|
if !strings.Contains(primary, `acl "internal" { 10.0.0.0/8; 192.168.0.0/16; };`) {
|
||||||
|
t.Fatalf("ACL not rendered correctly:\n%s", primary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCatalogHashStable(t *testing.T) {
|
||||||
|
// SHA-1 of the wire format of "example.com" is well-defined and stable.
|
||||||
|
h1 := catalogHash("example.com")
|
||||||
|
h2 := catalogHash("example.com.")
|
||||||
|
if h1 != h2 {
|
||||||
|
t.Fatalf("trailing dot should not change hash: %s vs %s", h1, h2)
|
||||||
|
}
|
||||||
|
if len(h1) != 40 {
|
||||||
|
t.Fatalf("expected 40-char hex sha1, got %d: %s", len(h1), h1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package bind
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"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}
|
||||||
|
return e.Exec(ctx, namespace, pod, append(base, args...), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reconfig reloads named.conf and any newly added/removed zones without a full
|
||||||
|
// restart.
|
||||||
|
func (e *Executor) Reconfig(ctx context.Context, namespace, pod string) error {
|
||||||
|
_, err := e.Rndc(ctx, namespace, pod, "reconfig")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddZone provisions a zone at runtime via `rndc addzone`. config is the inner
|
||||||
|
// zone clause, e.g. `{ type primary; file "db.example"; allow-update { key k; }; };`.
|
||||||
|
func (e *Executor) AddZone(ctx context.Context, namespace, pod, zone, view, config string) error {
|
||||||
|
args := []string{"addzone", zone}
|
||||||
|
if view != "" {
|
||||||
|
args = append(args, "in", view)
|
||||||
|
}
|
||||||
|
args = append(args, config)
|
||||||
|
out, err := e.Rndc(ctx, namespace, pod, args...)
|
||||||
|
if err != nil {
|
||||||
|
// addzone fails if the zone already exists; fall back to modzone so the
|
||||||
|
// operation is idempotent.
|
||||||
|
if strings.Contains(err.Error(), "already exists") || strings.Contains(out, "already exists") {
|
||||||
|
return e.ModZone(ctx, namespace, pod, zone, view, config)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModZone updates an existing runtime-added zone's configuration.
|
||||||
|
func (e *Executor) ModZone(ctx context.Context, namespace, pod, zone, view, config string) error {
|
||||||
|
args := []string{"modzone", zone}
|
||||||
|
if view != "" {
|
||||||
|
args = append(args, "in", view)
|
||||||
|
}
|
||||||
|
args = append(args, config)
|
||||||
|
_, err := e.Rndc(ctx, namespace, pod, args...)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DelZone removes a runtime-added zone. A missing zone is treated as success.
|
||||||
|
func (e *Executor) DelZone(ctx context.Context, namespace, pod, zone, view string) error {
|
||||||
|
args := []string{"delzone", zone}
|
||||||
|
if view != "" {
|
||||||
|
args = append(args, "in", view)
|
||||||
|
}
|
||||||
|
out, err := e.Rndc(ctx, namespace, pod, args...)
|
||||||
|
if err != nil && (strings.Contains(err.Error(), "not found") || strings.Contains(out, "not found")) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZoneSerial returns the current SOA serial for a zone via `rndc zonestatus`.
|
||||||
|
func (e *Executor) ZoneSerial(ctx context.Context, namespace, pod, zone, view string) (int64, error) {
|
||||||
|
args := []string{"zonestatus", zone}
|
||||||
|
if view != "" {
|
||||||
|
args = append(args, "in", view)
|
||||||
|
}
|
||||||
|
out, err := e.Rndc(ctx, namespace, pod, args...)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
for _, line := range strings.Split(out, "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(line, "serial:") {
|
||||||
|
var serial int64
|
||||||
|
if _, err := fmt.Sscanf(line, "serial: %d", &serial); err == nil {
|
||||||
|
return serial, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package bind
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ZoneFilePath returns the on-pod path of a zone database file.
|
||||||
|
func ZoneFilePath(zone string) string {
|
||||||
|
return fmt.Sprintf("%s/zones/db.%s", DataDir, strings.TrimSuffix(zone, "."))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CatalogFilePath returns the on-pod path of a catalog zone database file.
|
||||||
|
func CatalogFilePath(zone string) string {
|
||||||
|
return fmt.Sprintf("%s/catalog/db.%s", DataDir, strings.TrimSuffix(zone, "."))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZoneExists reports whether a zone is currently loaded on the pod.
|
||||||
|
func (e *Executor) ZoneExists(ctx context.Context, namespace, pod, zone, view string) bool {
|
||||||
|
args := []string{"zonestatus", zone}
|
||||||
|
if view != "" {
|
||||||
|
args = append(args, "in", view)
|
||||||
|
}
|
||||||
|
_, err := e.Rndc(ctx, namespace, pod, args...)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteSeedZone writes a minimal loadable zone file (SOA + apex NS) to path,
|
||||||
|
// creating parent directories. It is only safe to call when creating a zone, as
|
||||||
|
// it overwrites any existing file.
|
||||||
|
func (e *Executor) WriteSeedZone(ctx context.Context, namespace, pod, zone, path, primaryNS string, serial int64) error {
|
||||||
|
origin := dot(zone)
|
||||||
|
if primaryNS == "" {
|
||||||
|
primaryNS = "ns1." + origin
|
||||||
|
}
|
||||||
|
content := fmt.Sprintf(`$TTL 3600
|
||||||
|
@ IN SOA %s hostmaster.%s (
|
||||||
|
%d ; serial
|
||||||
|
3600 ; refresh
|
||||||
|
900 ; retry
|
||||||
|
1209600 ; expire
|
||||||
|
300 ) ; minimum
|
||||||
|
@ IN NS %s
|
||||||
|
`, dot(primaryNS), origin, serial, dot(primaryNS))
|
||||||
|
|
||||||
|
cmd := []string{"sh", "-c", fmt.Sprintf("mkdir -p \"$(dirname '%s')\" && cat > '%s'", path, path)}
|
||||||
|
if out, err := e.Exec(ctx, namespace, pod, cmd, content); err != nil {
|
||||||
|
return fmt.Errorf("seed zone %s: %w (out: %s)", zone, err, out)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddCatalogMember registers a member zone in a catalog zone by adding the
|
||||||
|
// catalog PTR record, so secondaries auto-provision it.
|
||||||
|
func (e *Executor) AddCatalogMember(ctx context.Context, namespace, pod, catalogZone, memberZone string, creds TSIGCreds) error {
|
||||||
|
hash := catalogHash(memberZone)
|
||||||
|
owner := fmt.Sprintf("%s.zones.%s", hash, dot(catalogZone))
|
||||||
|
updates := []RecordUpdate{{
|
||||||
|
FQDN: owner,
|
||||||
|
Type: "PTR",
|
||||||
|
TTL: 3600,
|
||||||
|
Values: []string{dot(memberZone)},
|
||||||
|
}}
|
||||||
|
return e.NSUpdate(ctx, namespace, pod, catalogZone, creds, updates)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveCatalogMember deregisters a member zone from a catalog zone.
|
||||||
|
func (e *Executor) RemoveCatalogMember(ctx context.Context, namespace, pod, catalogZone, memberZone string, creds TSIGCreds) error {
|
||||||
|
hash := catalogHash(memberZone)
|
||||||
|
owner := fmt.Sprintf("%s.zones.%s", hash, dot(catalogZone))
|
||||||
|
updates := []RecordUpdate{{FQDN: owner, Type: "PTR", Delete: true}}
|
||||||
|
return e.NSUpdate(ctx, namespace, pod, catalogZone, creds, updates)
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
|
||||||
|
bindv1alpha1 "git.unkin.net/unkin/bind-operator/api/v1alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BindACLReconciler validates a BindACL and reports readiness. The rendered ACL
|
||||||
|
// is emitted into named.conf by the BindCluster controller, which watches ACLs.
|
||||||
|
type BindACLReconciler struct {
|
||||||
|
client.Client
|
||||||
|
Scheme *runtime.Scheme
|
||||||
|
}
|
||||||
|
|
||||||
|
// +kubebuilder:rbac:groups=bind.unkin.net,resources=bindacls,verbs=get;list;watch;create;update;patch;delete
|
||||||
|
// +kubebuilder:rbac:groups=bind.unkin.net,resources=bindacls/status,verbs=get;update;patch
|
||||||
|
|
||||||
|
func (r *BindACLReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||||
|
var acl bindv1alpha1.BindACL
|
||||||
|
if err := r.Get(ctx, req.NamespacedName, &acl); err != nil {
|
||||||
|
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||||
|
}
|
||||||
|
acl.Status.Ready = len(acl.Spec.Entries) > 0
|
||||||
|
acl.Status.ObservedGeneration = acl.Generation
|
||||||
|
setReady(&acl.Status.Conditions, acl.Generation, acl.Status.Ready, "Validated", "ACL rendered into named.conf")
|
||||||
|
if err := r.Status().Update(ctx, &acl); err != nil {
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BindACLReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||||
|
return ctrl.NewControllerManagedBy(mgr).
|
||||||
|
For(&bindv1alpha1.BindACL{}).
|
||||||
|
Complete(r)
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||||
|
|
||||||
|
bindv1alpha1 "git.unkin.net/unkin/bind-operator/api/v1alpha1"
|
||||||
|
"git.unkin.net/unkin/bind-operator/internal/bind"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BindCatalogZoneReconciler creates and maintains the catalog zone on a cluster
|
||||||
|
// primary so secondaries auto-provision member zones.
|
||||||
|
type BindCatalogZoneReconciler struct {
|
||||||
|
client.Client
|
||||||
|
Scheme *runtime.Scheme
|
||||||
|
Exec *bind.Executor
|
||||||
|
}
|
||||||
|
|
||||||
|
// +kubebuilder:rbac:groups=bind.unkin.net,resources=bindcatalogzones,verbs=get;list;watch;create;update;patch;delete
|
||||||
|
// +kubebuilder:rbac:groups=bind.unkin.net,resources=bindcatalogzones/status,verbs=get;update;patch
|
||||||
|
// +kubebuilder:rbac:groups=bind.unkin.net,resources=bindzones;bindtsigkeys,verbs=get;list;watch
|
||||||
|
|
||||||
|
func (r *BindCatalogZoneReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||||
|
logger := log.FromContext(ctx)
|
||||||
|
|
||||||
|
var catalog bindv1alpha1.BindCatalogZone
|
||||||
|
if err := r.Get(ctx, req.NamespacedName, &catalog); err != nil {
|
||||||
|
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cluster, err := getCluster(ctx, r.Client, catalog.Namespace, catalog.Spec.ClusterRef)
|
||||||
|
if err != nil {
|
||||||
|
return r.fail(ctx, &catalog, "ClusterMissing", err.Error())
|
||||||
|
}
|
||||||
|
primaryPod := primaryPodName(cluster.Name)
|
||||||
|
|
||||||
|
if !primaryReady(ctx, r.Client, cluster) || r.Exec == nil {
|
||||||
|
return r.fail(ctx, &catalog, "PrimaryNotReady", "waiting for cluster primary")
|
||||||
|
}
|
||||||
|
|
||||||
|
creds, err := resolveTSIG(ctx, r.Client, catalog.Namespace, catalog.Spec.TransferKeyRef)
|
||||||
|
if err != nil {
|
||||||
|
return r.fail(ctx, &catalog, "NoTransferKey", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the catalog zone exists on the primary.
|
||||||
|
if !r.Exec.ZoneExists(ctx, catalog.Namespace, primaryPod, catalog.Spec.ZoneName, "") {
|
||||||
|
if err := r.Exec.WriteSeedZone(ctx, catalog.Namespace, primaryPod, catalog.Spec.ZoneName, bind.CatalogFilePath(catalog.Spec.ZoneName), "", 1); err != nil {
|
||||||
|
return r.fail(ctx, &catalog, "SeedFailed", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
zoneConfig := fmt.Sprintf("{ type primary; file \"%s\"; allow-transfer { key \"%s\"; }; allow-update { key \"%s\"; }; };",
|
||||||
|
bind.CatalogFilePath(catalog.Spec.ZoneName), catalog.Spec.TransferKeyRef, catalog.Spec.TransferKeyRef)
|
||||||
|
if err := r.Exec.AddZone(ctx, catalog.Namespace, primaryPod, catalog.Spec.ZoneName, "", zoneConfig); err != nil {
|
||||||
|
return r.fail(ctx, &catalog, "AddZoneFailed", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Catalog zones must advertise their schema version (RFC 9432: "2").
|
||||||
|
versionUpdate := bind.RecordUpdate{
|
||||||
|
FQDN: "version." + catalog.Spec.ZoneName + ".",
|
||||||
|
Type: "TXT",
|
||||||
|
TTL: 3600,
|
||||||
|
Values: []string{"\"2\""},
|
||||||
|
}
|
||||||
|
if err := r.Exec.NSUpdate(ctx, catalog.Namespace, primaryPod, catalog.Spec.ZoneName, creds, []bind.RecordUpdate{versionUpdate}); err != nil {
|
||||||
|
return r.fail(ctx, &catalog, "VersionUpdateFailed", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count member zones for status.
|
||||||
|
var zones bindv1alpha1.BindZoneList
|
||||||
|
members := int32(0)
|
||||||
|
if err := r.List(ctx, &zones, client.InNamespace(catalog.Namespace)); err == nil {
|
||||||
|
for i := range zones.Items {
|
||||||
|
z := &zones.Items[i]
|
||||||
|
if z.Spec.ClusterRef == cluster.Name && catalogEnabled(z) {
|
||||||
|
members++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
catalog.Status.Ready = true
|
||||||
|
catalog.Status.MemberCount = members
|
||||||
|
catalog.Status.ObservedGeneration = catalog.Generation
|
||||||
|
setReady(&catalog.Status.Conditions, catalog.Generation, true, "Ready", "catalog zone provisioned")
|
||||||
|
if err := r.Status().Update(ctx, &catalog); err != nil {
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
logger.Info("catalog zone reconciled", "zone", catalog.Spec.ZoneName, "members", members)
|
||||||
|
return ctrl.Result{RequeueAfter: requeueLong}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BindCatalogZoneReconciler) fail(ctx context.Context, catalog *bindv1alpha1.BindCatalogZone, reason, msg string) (ctrl.Result, error) {
|
||||||
|
catalog.Status.Ready = false
|
||||||
|
catalog.Status.ObservedGeneration = catalog.Generation
|
||||||
|
setReady(&catalog.Status.Conditions, catalog.Generation, false, reason, msg)
|
||||||
|
if err := r.Status().Update(ctx, catalog); err != nil {
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
return ctrl.Result{RequeueAfter: requeueShort}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BindCatalogZoneReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||||
|
return ctrl.NewControllerManagedBy(mgr).
|
||||||
|
For(&bindv1alpha1.BindCatalogZone{}).
|
||||||
|
Complete(r)
|
||||||
|
}
|
||||||
@@ -0,0 +1,418 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
"k8s.io/apimachinery/pkg/api/resource"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||||
|
|
||||||
|
bindv1alpha1 "git.unkin.net/unkin/bind-operator/api/v1alpha1"
|
||||||
|
"git.unkin.net/unkin/bind-operator/internal/bind"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BindClusterReconciler manages the StatefulSet, Services, ConfigMap and
|
||||||
|
// Secrets backing a BindCluster, and re-renders named.conf when dependent
|
||||||
|
// objects (ACLs, views, policies, keys, catalog) change.
|
||||||
|
type BindClusterReconciler struct {
|
||||||
|
client.Client
|
||||||
|
Scheme *runtime.Scheme
|
||||||
|
Exec *bind.Executor
|
||||||
|
}
|
||||||
|
|
||||||
|
// +kubebuilder:rbac:groups=bind.unkin.net,resources=bindclusters,verbs=get;list;watch;create;update;patch;delete
|
||||||
|
// +kubebuilder:rbac:groups=bind.unkin.net,resources=bindclusters/status,verbs=get;update;patch
|
||||||
|
// +kubebuilder:rbac:groups=bind.unkin.net,resources=bindacls;bindviews;bindpolicies;binddnssecpolicies;bindcatalogzones;bindtsigkeys,verbs=get;list;watch
|
||||||
|
// +kubebuilder:rbac:groups=apps,resources=statefulsets,verbs=get;list;watch;create;update;patch;delete
|
||||||
|
// +kubebuilder:rbac:groups="",resources=services;configmaps;secrets,verbs=get;list;watch;create;update;patch;delete
|
||||||
|
// +kubebuilder:rbac:groups="",resources=pods,verbs=get;list;watch
|
||||||
|
// +kubebuilder:rbac:groups="",resources=pods/exec,verbs=create;get
|
||||||
|
|
||||||
|
func (r *BindClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||||
|
logger := log.FromContext(ctx)
|
||||||
|
|
||||||
|
var cluster bindv1alpha1.BindCluster
|
||||||
|
if err := r.Get(ctx, req.NamespacedName, &cluster); err != nil {
|
||||||
|
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.reconcileRNDCSecret(ctx, &cluster); err != nil {
|
||||||
|
return ctrl.Result{}, fmt.Errorf("rndc secret: %w", err)
|
||||||
|
}
|
||||||
|
if err := r.reconcileKeysSecret(ctx, &cluster); err != nil {
|
||||||
|
return ctrl.Result{}, fmt.Errorf("keys secret: %w", err)
|
||||||
|
}
|
||||||
|
if err := r.reconcileConfigMap(ctx, &cluster); err != nil {
|
||||||
|
return ctrl.Result{}, fmt.Errorf("configmap: %w", err)
|
||||||
|
}
|
||||||
|
if err := r.reconcileServices(ctx, &cluster); err != nil {
|
||||||
|
return ctrl.Result{}, fmt.Errorf("services: %w", err)
|
||||||
|
}
|
||||||
|
sts, err := r.reconcileStatefulSet(ctx, &cluster)
|
||||||
|
if err != nil {
|
||||||
|
return ctrl.Result{}, fmt.Errorf("statefulset: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Best-effort: reload configuration on ready pods so ConfigMap changes take
|
||||||
|
// effect without a rollout.
|
||||||
|
r.reloadReadyPods(ctx, &cluster)
|
||||||
|
|
||||||
|
// Status.
|
||||||
|
cluster.Status.ObservedGeneration = cluster.Generation
|
||||||
|
cluster.Status.Replicas = cluster.Spec.Replicas
|
||||||
|
cluster.Status.ReadyReplicas = sts.Status.ReadyReplicas
|
||||||
|
cluster.Status.PrimaryPod = primaryPodName(cluster.Name)
|
||||||
|
cluster.Status.PrimaryService = primaryAddress(cluster.Name, cluster.Namespace)
|
||||||
|
ready := sts.Status.ReadyReplicas == cluster.Spec.Replicas && cluster.Spec.Replicas > 0
|
||||||
|
if ready {
|
||||||
|
cluster.Status.Phase = "Ready"
|
||||||
|
} else {
|
||||||
|
cluster.Status.Phase = "Progressing"
|
||||||
|
}
|
||||||
|
setReady(&cluster.Status.Conditions, cluster.Generation, ready, "Reconciled",
|
||||||
|
fmt.Sprintf("%d/%d replicas ready", sts.Status.ReadyReplicas, cluster.Spec.Replicas))
|
||||||
|
if err := r.Status().Update(ctx, &cluster); err != nil {
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ready {
|
||||||
|
return ctrl.Result{RequeueAfter: requeueShort}, nil
|
||||||
|
}
|
||||||
|
logger.V(1).Info("cluster reconciled", "cluster", cluster.Name, "ready", sts.Status.ReadyReplicas)
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BindClusterReconciler) reconcileRNDCSecret(ctx context.Context, c *bindv1alpha1.BindCluster) error {
|
||||||
|
name := rndcSecretName(c.Name)
|
||||||
|
var existing corev1.Secret
|
||||||
|
err := r.Get(ctx, types.NamespacedName{Namespace: c.Namespace, Name: name}, &existing)
|
||||||
|
if err == nil {
|
||||||
|
return nil // rndc key is generated once and preserved
|
||||||
|
}
|
||||||
|
if !apierrors.IsNotFound(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
secret, genErr := bind.GenerateSecret(32)
|
||||||
|
if genErr != nil {
|
||||||
|
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")
|
||||||
|
s := &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: c.Namespace, Labels: commonLabels(c.Name)},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"rndc.key": []byte(keyClause),
|
||||||
|
"rndc.conf": []byte(rndcConf),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := ctrl.SetControllerReference(c, s, r.Scheme); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return r.Create(ctx, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BindClusterReconciler) reconcileKeysSecret(ctx context.Context, c *bindv1alpha1.BindCluster) error {
|
||||||
|
var keys bindv1alpha1.BindTSIGKeyList
|
||||||
|
if err := r.List(ctx, &keys, client.InNamespace(c.Namespace)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
items := append([]bindv1alpha1.BindTSIGKey(nil), keys.Items...)
|
||||||
|
sort.Slice(items, func(i, j int) bool { return items[i].Name < items[j].Name })
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString("// Managed by bind-operator.\n")
|
||||||
|
for _, k := range items {
|
||||||
|
secretName := k.Status.SecretName
|
||||||
|
if secretName == "" {
|
||||||
|
secretName = k.Spec.SecretName
|
||||||
|
}
|
||||||
|
if secretName == "" {
|
||||||
|
secretName = k.Name + "-tsig"
|
||||||
|
}
|
||||||
|
var secret corev1.Secret
|
||||||
|
if err := r.Get(ctx, types.NamespacedName{Namespace: c.Namespace, Name: secretName}, &secret); err != nil {
|
||||||
|
continue // key not yet materialised; skip until its controller runs
|
||||||
|
}
|
||||||
|
keyName := k.Spec.KeyName
|
||||||
|
if keyName == "" {
|
||||||
|
keyName = k.Name
|
||||||
|
}
|
||||||
|
alg := string(secret.Data["algorithm"])
|
||||||
|
if alg == "" {
|
||||||
|
alg = string(bindv1alpha1.TSIGHMACSHA256)
|
||||||
|
}
|
||||||
|
b.WriteString(bind.KeyClause(keyName, alg, string(secret.Data["secret"])))
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.upsertSecret(ctx, c, keysSecretName(c.Name), map[string][]byte{"keys.conf": []byte(b.String())})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BindClusterReconciler) reconcileConfigMap(ctx context.Context, c *bindv1alpha1.BindCluster) error {
|
||||||
|
in := bind.RenderInput{Cluster: c, PrimaryAddress: primaryAddress(c.Name, c.Namespace)}
|
||||||
|
|
||||||
|
var acls bindv1alpha1.BindACLList
|
||||||
|
if err := r.List(ctx, &acls, client.InNamespace(c.Namespace)); err == nil {
|
||||||
|
for _, a := range acls.Items {
|
||||||
|
if a.Spec.ClusterRef == "" || a.Spec.ClusterRef == c.Name {
|
||||||
|
in.ACLs = append(in.ACLs, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var views bindv1alpha1.BindViewList
|
||||||
|
if err := r.List(ctx, &views, client.InNamespace(c.Namespace)); err == nil {
|
||||||
|
for _, v := range views.Items {
|
||||||
|
if v.Spec.ClusterRef == c.Name {
|
||||||
|
in.Views = append(in.Views, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var policies bindv1alpha1.BindPolicyList
|
||||||
|
if err := r.List(ctx, &policies, client.InNamespace(c.Namespace)); err == nil {
|
||||||
|
for _, p := range policies.Items {
|
||||||
|
if p.Spec.ClusterRef == c.Name {
|
||||||
|
in.Policies = append(in.Policies, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var dnssec bindv1alpha1.BindDNSSECPolicyList
|
||||||
|
if err := r.List(ctx, &dnssec, client.InNamespace(c.Namespace)); err == nil {
|
||||||
|
for _, d := range dnssec.Items {
|
||||||
|
if d.Spec.ClusterRef == c.Name {
|
||||||
|
in.DNSSECPolicies = append(in.DNSSECPolicies, d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var catalogs bindv1alpha1.BindCatalogZoneList
|
||||||
|
if err := r.List(ctx, &catalogs, client.InNamespace(c.Namespace)); err == nil {
|
||||||
|
for i := range catalogs.Items {
|
||||||
|
if catalogs.Items[i].Spec.ClusterRef == c.Name {
|
||||||
|
in.Catalog = &catalogs.Items[i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
primaryConf, secondaryConf := bind.RenderNamedConf(in)
|
||||||
|
data := map[string]string{
|
||||||
|
"named.conf.primary": primaryConf,
|
||||||
|
"named.conf.secondary": secondaryConf,
|
||||||
|
"entrypoint.sh": entrypointScript(),
|
||||||
|
}
|
||||||
|
return r.upsertConfigMap(ctx, c, configMapName(c.Name), data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BindClusterReconciler) reconcileServices(ctx context.Context, c *bindv1alpha1.BindCluster) error {
|
||||||
|
dnsPorts := []corev1.ServicePort{
|
||||||
|
{Name: "dns-udp", Port: 53, Protocol: corev1.ProtocolUDP, TargetPort: intstrFromInt(53)},
|
||||||
|
{Name: "dns-tcp", Port: 53, Protocol: corev1.ProtocolTCP, TargetPort: intstrFromInt(53)},
|
||||||
|
}
|
||||||
|
|
||||||
|
headless := &corev1.Service{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: headlessServiceName(c.Name), Namespace: c.Namespace, Labels: commonLabels(c.Name)},
|
||||||
|
Spec: corev1.ServiceSpec{
|
||||||
|
ClusterIP: corev1.ClusterIPNone,
|
||||||
|
PublishNotReadyAddresses: true,
|
||||||
|
Selector: commonLabels(c.Name),
|
||||||
|
Ports: dnsPorts,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := r.upsertService(ctx, c, headless); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
svcType := c.Spec.Service.Type
|
||||||
|
if svcType == "" {
|
||||||
|
svcType = corev1.ServiceTypeClusterIP
|
||||||
|
}
|
||||||
|
client := &corev1.Service{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: clientServiceName(c.Name),
|
||||||
|
Namespace: c.Namespace,
|
||||||
|
Labels: commonLabels(c.Name),
|
||||||
|
Annotations: c.Spec.Service.Annotations,
|
||||||
|
},
|
||||||
|
Spec: corev1.ServiceSpec{
|
||||||
|
Type: svcType,
|
||||||
|
Selector: commonLabels(c.Name),
|
||||||
|
Ports: dnsPorts,
|
||||||
|
LoadBalancerIP: c.Spec.Service.LoadBalancerIP,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return r.upsertService(ctx, c, client)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BindClusterReconciler) reconcileStatefulSet(ctx context.Context, c *bindv1alpha1.BindCluster) (*appsv1.StatefulSet, error) {
|
||||||
|
labels := commonLabels(c.Name)
|
||||||
|
replicas := c.Spec.Replicas
|
||||||
|
image := c.Spec.Image
|
||||||
|
if image == "" {
|
||||||
|
image = "git.unkin.net/unkin/bind9:latest"
|
||||||
|
}
|
||||||
|
storageSize := c.Spec.StorageSize
|
||||||
|
if storageSize == "" {
|
||||||
|
storageSize = "1Gi"
|
||||||
|
}
|
||||||
|
qty, err := resource.ParseQuantity(storageSize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse storageSize: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
projected := corev1.Volume{
|
||||||
|
Name: "bind-etc",
|
||||||
|
VolumeSource: corev1.VolumeSource{Projected: &corev1.ProjectedVolumeSource{Sources: []corev1.VolumeProjection{
|
||||||
|
{ConfigMap: &corev1.ConfigMapProjection{LocalObjectReference: corev1.LocalObjectReference{Name: configMapName(c.Name)}}},
|
||||||
|
{Secret: &corev1.SecretProjection{LocalObjectReference: corev1.LocalObjectReference{Name: keysSecretName(c.Name)}}},
|
||||||
|
{Secret: &corev1.SecretProjection{LocalObjectReference: corev1.LocalObjectReference{Name: rndcSecretName(c.Name)}}},
|
||||||
|
}}},
|
||||||
|
}
|
||||||
|
|
||||||
|
sts := &appsv1.StatefulSet{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: c.Name, Namespace: c.Namespace, Labels: labels},
|
||||||
|
Spec: appsv1.StatefulSetSpec{
|
||||||
|
ServiceName: headlessServiceName(c.Name),
|
||||||
|
Replicas: &replicas,
|
||||||
|
Selector: &metav1.LabelSelector{MatchLabels: labels},
|
||||||
|
Template: corev1.PodTemplateSpec{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Labels: labels},
|
||||||
|
Spec: corev1.PodSpec{
|
||||||
|
NodeSelector: c.Spec.NodeSelector,
|
||||||
|
Tolerations: c.Spec.Tolerations,
|
||||||
|
Affinity: c.Spec.Affinity,
|
||||||
|
Containers: []corev1.Container{{
|
||||||
|
Name: bind.ContainerName,
|
||||||
|
Image: image,
|
||||||
|
ImagePullPolicy: c.Spec.ImagePullPolicy,
|
||||||
|
Command: []string{"/bin/sh", "/etc/bind/entrypoint.sh"},
|
||||||
|
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: "data", MountPath: bind.DataDir},
|
||||||
|
},
|
||||||
|
ReadinessProbe: &corev1.Probe{
|
||||||
|
ProbeHandler: corev1.ProbeHandler{TCPSocket: &corev1.TCPSocketAction{Port: intstrFromInt(53)}},
|
||||||
|
InitialDelaySeconds: 5,
|
||||||
|
PeriodSeconds: 10,
|
||||||
|
},
|
||||||
|
LivenessProbe: &corev1.Probe{
|
||||||
|
ProbeHandler: corev1.ProbeHandler{TCPSocket: &corev1.TCPSocketAction{Port: intstrFromInt(53)}},
|
||||||
|
InitialDelaySeconds: 15,
|
||||||
|
PeriodSeconds: 20,
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
Volumes: []corev1.Volume{
|
||||||
|
projected,
|
||||||
|
{Name: "run", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
VolumeClaimTemplates: []corev1.PersistentVolumeClaim{{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "data"},
|
||||||
|
Spec: corev1.PersistentVolumeClaimSpec{
|
||||||
|
AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce},
|
||||||
|
StorageClassName: c.Spec.StorageClassName,
|
||||||
|
Resources: corev1.VolumeResourceRequirements{Requests: corev1.ResourceList{corev1.ResourceStorage: qty}},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := ctrl.SetControllerReference(c, sts, r.Scheme); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var existing appsv1.StatefulSet
|
||||||
|
err = r.Get(ctx, types.NamespacedName{Namespace: c.Namespace, Name: c.Name}, &existing)
|
||||||
|
if apierrors.IsNotFound(err) {
|
||||||
|
return sts, r.Create(ctx, sts)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// VolumeClaimTemplates are immutable; only mutate the mutable fields.
|
||||||
|
existing.Spec.Replicas = sts.Spec.Replicas
|
||||||
|
existing.Spec.Template = sts.Spec.Template
|
||||||
|
if err := r.Update(ctx, &existing); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &existing, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BindClusterReconciler) reloadReadyPods(ctx context.Context, c *bindv1alpha1.BindCluster) {
|
||||||
|
if r.Exec == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger := log.FromContext(ctx)
|
||||||
|
var pods corev1.PodList
|
||||||
|
if err := r.List(ctx, &pods, client.InNamespace(c.Namespace), client.MatchingLabels(commonLabels(c.Name))); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i := range pods.Items {
|
||||||
|
pod := &pods.Items[i]
|
||||||
|
if !podReady(pod) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := r.Exec.Reconfig(ctx, c.Namespace, pod.Name); err != nil {
|
||||||
|
logger.V(1).Info("rndc reconfig failed", "pod", pod.Name, "err", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BindClusterReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||||
|
mapToCluster := func(clusterRef, namespace string) []reconcile.Request {
|
||||||
|
if clusterRef == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []reconcile.Request{{NamespacedName: types.NamespacedName{Namespace: namespace, Name: clusterRef}}}
|
||||||
|
}
|
||||||
|
return ctrl.NewControllerManagedBy(mgr).
|
||||||
|
For(&bindv1alpha1.BindCluster{}).
|
||||||
|
Owns(&appsv1.StatefulSet{}).
|
||||||
|
Owns(&corev1.Service{}).
|
||||||
|
Owns(&corev1.ConfigMap{}).
|
||||||
|
Owns(&corev1.Secret{}).
|
||||||
|
Watches(&bindv1alpha1.BindACL{}, handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, o client.Object) []reconcile.Request {
|
||||||
|
return mapToCluster(o.(*bindv1alpha1.BindACL).Spec.ClusterRef, o.GetNamespace())
|
||||||
|
})).
|
||||||
|
Watches(&bindv1alpha1.BindView{}, handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, o client.Object) []reconcile.Request {
|
||||||
|
return mapToCluster(o.(*bindv1alpha1.BindView).Spec.ClusterRef, o.GetNamespace())
|
||||||
|
})).
|
||||||
|
Watches(&bindv1alpha1.BindPolicy{}, handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, o client.Object) []reconcile.Request {
|
||||||
|
return mapToCluster(o.(*bindv1alpha1.BindPolicy).Spec.ClusterRef, o.GetNamespace())
|
||||||
|
})).
|
||||||
|
Watches(&bindv1alpha1.BindDNSSECPolicy{}, handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, o client.Object) []reconcile.Request {
|
||||||
|
return mapToCluster(o.(*bindv1alpha1.BindDNSSECPolicy).Spec.ClusterRef, o.GetNamespace())
|
||||||
|
})).
|
||||||
|
Watches(&bindv1alpha1.BindCatalogZone{}, handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, o client.Object) []reconcile.Request {
|
||||||
|
return mapToCluster(o.(*bindv1alpha1.BindCatalogZone).Spec.ClusterRef, o.GetNamespace())
|
||||||
|
})).
|
||||||
|
Watches(&bindv1alpha1.BindTSIGKey{}, handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, o client.Object) []reconcile.Request {
|
||||||
|
// TSIG keys are namespace-wide; re-render every cluster in the namespace.
|
||||||
|
var clusters bindv1alpha1.BindClusterList
|
||||||
|
if err := r.List(ctx, &clusters, client.InNamespace(o.GetNamespace())); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var reqs []reconcile.Request
|
||||||
|
for _, cl := range clusters.Items {
|
||||||
|
reqs = append(reqs, reconcile.Request{NamespacedName: types.NamespacedName{Namespace: cl.Namespace, Name: cl.Name}})
|
||||||
|
}
|
||||||
|
return reqs
|
||||||
|
})).
|
||||||
|
Complete(r)
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
|
||||||
|
bindv1alpha1 "git.unkin.net/unkin/bind-operator/api/v1alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BindDNSSECPolicyReconciler validates a signing policy and reports how many
|
||||||
|
// zones reference it. The dnssec-policy block is rendered into named.conf by
|
||||||
|
// the BindCluster controller, which watches these policies.
|
||||||
|
type BindDNSSECPolicyReconciler struct {
|
||||||
|
client.Client
|
||||||
|
Scheme *runtime.Scheme
|
||||||
|
}
|
||||||
|
|
||||||
|
// +kubebuilder:rbac:groups=bind.unkin.net,resources=binddnssecpolicies,verbs=get;list;watch;create;update;patch;delete
|
||||||
|
// +kubebuilder:rbac:groups=bind.unkin.net,resources=binddnssecpolicies/status,verbs=get;update;patch
|
||||||
|
// +kubebuilder:rbac:groups=bind.unkin.net,resources=bindzones,verbs=get;list;watch
|
||||||
|
|
||||||
|
func (r *BindDNSSECPolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||||
|
var policy bindv1alpha1.BindDNSSECPolicy
|
||||||
|
if err := r.Get(ctx, req.NamespacedName, &policy); err != nil {
|
||||||
|
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var zones bindv1alpha1.BindZoneList
|
||||||
|
count := int32(0)
|
||||||
|
if err := r.List(ctx, &zones, client.InNamespace(policy.Namespace)); err == nil {
|
||||||
|
for _, z := range zones.Items {
|
||||||
|
if z.Spec.ClusterRef == policy.Spec.ClusterRef && z.Spec.DNSSECPolicyRef == policy.Name {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
policy.Status.ZoneCount = count
|
||||||
|
policy.Status.Ready = policy.Spec.ClusterRef != ""
|
||||||
|
policy.Status.ObservedGeneration = policy.Generation
|
||||||
|
setReady(&policy.Status.Conditions, policy.Generation, policy.Status.Ready, "Validated", "dnssec-policy rendered into named.conf")
|
||||||
|
if err := r.Status().Update(ctx, &policy); err != nil {
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BindDNSSECPolicyReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||||
|
return ctrl.NewControllerManagedBy(mgr).
|
||||||
|
For(&bindv1alpha1.BindDNSSECPolicy{}).
|
||||||
|
Complete(r)
|
||||||
|
}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||||
|
|
||||||
|
bindv1alpha1 "git.unkin.net/unkin/bind-operator/api/v1alpha1"
|
||||||
|
"git.unkin.net/unkin/bind-operator/internal/bind"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BindPolicyReconciler provisions a Response Policy Zone (RPZ) on a cluster
|
||||||
|
// primary and seeds its rules. The cluster controller renders the matching
|
||||||
|
// response-policy clause into named.conf.
|
||||||
|
type BindPolicyReconciler struct {
|
||||||
|
client.Client
|
||||||
|
Scheme *runtime.Scheme
|
||||||
|
Exec *bind.Executor
|
||||||
|
}
|
||||||
|
|
||||||
|
// +kubebuilder:rbac:groups=bind.unkin.net,resources=bindpolicies,verbs=get;list;watch;create;update;patch;delete
|
||||||
|
// +kubebuilder:rbac:groups=bind.unkin.net,resources=bindpolicies/status,verbs=get;update;patch
|
||||||
|
// +kubebuilder:rbac:groups=bind.unkin.net,resources=bindtsigkeys,verbs=get;list;watch
|
||||||
|
|
||||||
|
func (r *BindPolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||||
|
logger := log.FromContext(ctx)
|
||||||
|
|
||||||
|
var policy bindv1alpha1.BindPolicy
|
||||||
|
if err := r.Get(ctx, req.NamespacedName, &policy); err != nil {
|
||||||
|
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cluster, err := getCluster(ctx, r.Client, policy.Namespace, policy.Spec.ClusterRef)
|
||||||
|
if err != nil {
|
||||||
|
return r.fail(ctx, &policy, "ClusterMissing", err.Error())
|
||||||
|
}
|
||||||
|
primaryPod := primaryPodName(cluster.Name)
|
||||||
|
if !primaryReady(ctx, r.Client, cluster) || r.Exec == nil {
|
||||||
|
return r.fail(ctx, &policy, "PrimaryNotReady", "waiting for cluster primary")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Externally-fed RPZ: configure as a secondary of the feed. Otherwise host a
|
||||||
|
// locally-populated primary RPZ zone.
|
||||||
|
if len(policy.Spec.Primaries) > 0 {
|
||||||
|
creds, _ := resolveTSIG(ctx, r.Client, policy.Namespace, policy.Spec.TransferKeyRef)
|
||||||
|
_ = creds
|
||||||
|
cfg := fmt.Sprintf("{ type secondary; file \"%s\"; primaries { %s }; };",
|
||||||
|
bind.ZoneFilePath(policy.Spec.ZoneName), terminateInline(policy.Spec.Primaries))
|
||||||
|
if err := r.Exec.AddZone(ctx, policy.Namespace, primaryPod, policy.Spec.ZoneName, policy.Spec.ViewRef, cfg); err != nil {
|
||||||
|
return r.fail(ctx, &policy, "AddZoneFailed", err.Error())
|
||||||
|
}
|
||||||
|
return r.ready(ctx, &policy, int32(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
creds, err := resolveTSIG(ctx, r.Client, policy.Namespace, policy.Spec.TransferKeyRef)
|
||||||
|
if err != nil {
|
||||||
|
return r.fail(ctx, &policy, "NoUpdateKey", "spec.transferKeyRef required to seed RPZ rules")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !r.Exec.ZoneExists(ctx, policy.Namespace, primaryPod, policy.Spec.ZoneName, policy.Spec.ViewRef) {
|
||||||
|
if err := r.Exec.WriteSeedZone(ctx, policy.Namespace, primaryPod, policy.Spec.ZoneName, bind.ZoneFilePath(policy.Spec.ZoneName), "", 1); err != nil {
|
||||||
|
return r.fail(ctx, &policy, "SeedFailed", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cfg := fmt.Sprintf("{ type primary; file \"%s\"; allow-update { key \"%s\"; }; };",
|
||||||
|
bind.ZoneFilePath(policy.Spec.ZoneName), policy.Spec.TransferKeyRef)
|
||||||
|
if err := r.Exec.AddZone(ctx, policy.Namespace, primaryPod, policy.Spec.ZoneName, policy.Spec.ViewRef, cfg); err != nil {
|
||||||
|
return r.fail(ctx, &policy, "AddZoneFailed", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
updates := rpzRulesToUpdates(policy.Spec.ZoneName, policy.Spec.Rules)
|
||||||
|
if len(updates) > 0 {
|
||||||
|
if err := r.Exec.NSUpdate(ctx, policy.Namespace, primaryPod, policy.Spec.ZoneName, creds, updates); err != nil {
|
||||||
|
return r.fail(ctx, &policy, "RuleUpdateFailed", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.Info("policy reconciled", "zone", policy.Spec.ZoneName, "rules", len(updates))
|
||||||
|
return r.ready(ctx, &policy, int32(len(updates)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// rpzRulesToUpdates maps RPZ rules to the CNAME records that encode them.
|
||||||
|
func rpzRulesToUpdates(rpzZone string, rules []bindv1alpha1.RPZRule) []bind.RecordUpdate {
|
||||||
|
var updates []bind.RecordUpdate
|
||||||
|
origin := strings.TrimSuffix(rpzZone, ".") + "."
|
||||||
|
for _, rule := range rules {
|
||||||
|
trigger := rule.Trigger
|
||||||
|
if trigger == "" {
|
||||||
|
trigger = "qname"
|
||||||
|
}
|
||||||
|
match := strings.TrimSuffix(strings.TrimSpace(rule.Match), ".")
|
||||||
|
var owner string
|
||||||
|
switch trigger {
|
||||||
|
case "qname":
|
||||||
|
owner = match + "." + origin
|
||||||
|
case "client-ip":
|
||||||
|
owner = match + ".rpz-client-ip." + origin
|
||||||
|
case "ip":
|
||||||
|
owner = match + ".rpz-ip." + origin
|
||||||
|
case "nsdname":
|
||||||
|
owner = match + ".rpz-nsdname." + origin
|
||||||
|
case "nsip":
|
||||||
|
owner = match + ".rpz-nsip." + origin
|
||||||
|
default:
|
||||||
|
owner = match + "." + origin
|
||||||
|
}
|
||||||
|
|
||||||
|
action := rule.Action
|
||||||
|
if action == "" {
|
||||||
|
action = "nxdomain"
|
||||||
|
}
|
||||||
|
var rdata string
|
||||||
|
switch action {
|
||||||
|
case "nxdomain":
|
||||||
|
rdata = "."
|
||||||
|
case "nodata":
|
||||||
|
rdata = "*."
|
||||||
|
case "passthru":
|
||||||
|
rdata = "rpz-passthru."
|
||||||
|
case "drop":
|
||||||
|
rdata = "rpz-drop."
|
||||||
|
case "tcp-only":
|
||||||
|
rdata = "rpz-tcp-only."
|
||||||
|
case "cname":
|
||||||
|
rdata = strings.TrimSuffix(rule.Target, ".") + "."
|
||||||
|
default:
|
||||||
|
rdata = "."
|
||||||
|
}
|
||||||
|
updates = append(updates, bind.RecordUpdate{FQDN: owner, Type: "CNAME", TTL: 3600, Values: []string{rdata}})
|
||||||
|
}
|
||||||
|
return updates
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BindPolicyReconciler) ready(ctx context.Context, policy *bindv1alpha1.BindPolicy, rules int32) (ctrl.Result, error) {
|
||||||
|
policy.Status.Ready = true
|
||||||
|
policy.Status.RuleCount = rules
|
||||||
|
policy.Status.ObservedGeneration = policy.Generation
|
||||||
|
setReady(&policy.Status.Conditions, policy.Generation, true, "Ready", "RPZ provisioned")
|
||||||
|
if err := r.Status().Update(ctx, policy); err != nil {
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
return ctrl.Result{RequeueAfter: requeueLong}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BindPolicyReconciler) fail(ctx context.Context, policy *bindv1alpha1.BindPolicy, reason, msg string) (ctrl.Result, error) {
|
||||||
|
policy.Status.Ready = false
|
||||||
|
policy.Status.ObservedGeneration = policy.Generation
|
||||||
|
setReady(&policy.Status.Conditions, policy.Generation, false, reason, msg)
|
||||||
|
if err := r.Status().Update(ctx, policy); err != nil {
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
return ctrl.Result{RequeueAfter: requeueShort}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BindPolicyReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||||
|
return ctrl.NewControllerManagedBy(mgr).
|
||||||
|
For(&bindv1alpha1.BindPolicy{}).
|
||||||
|
Complete(r)
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
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/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||||
|
|
||||||
|
bindv1alpha1 "git.unkin.net/unkin/bind-operator/api/v1alpha1"
|
||||||
|
"git.unkin.net/unkin/bind-operator/internal/bind"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BindTSIGKeyReconciler generates TSIG key material into a Secret.
|
||||||
|
type BindTSIGKeyReconciler struct {
|
||||||
|
client.Client
|
||||||
|
Scheme *runtime.Scheme
|
||||||
|
}
|
||||||
|
|
||||||
|
// +kubebuilder:rbac:groups=bind.unkin.net,resources=bindtsigkeys,verbs=get;list;watch;create;update;patch;delete
|
||||||
|
// +kubebuilder:rbac:groups=bind.unkin.net,resources=bindtsigkeys/status,verbs=get;update;patch
|
||||||
|
// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete
|
||||||
|
|
||||||
|
func (r *BindTSIGKeyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||||
|
logger := log.FromContext(ctx)
|
||||||
|
|
||||||
|
var key bindv1alpha1.BindTSIGKey
|
||||||
|
if err := r.Get(ctx, req.NamespacedName, &key); err != nil {
|
||||||
|
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
algorithm := string(key.Spec.Algorithm)
|
||||||
|
if algorithm == "" {
|
||||||
|
algorithm = string(bindv1alpha1.TSIGHMACSHA256)
|
||||||
|
}
|
||||||
|
keyName := key.Spec.KeyName
|
||||||
|
if keyName == "" {
|
||||||
|
keyName = key.Name
|
||||||
|
}
|
||||||
|
secretName := key.Spec.SecretName
|
||||||
|
if secretName == "" {
|
||||||
|
secretName = key.Name + "-tsig"
|
||||||
|
}
|
||||||
|
|
||||||
|
var secret corev1.Secret
|
||||||
|
err := r.Get(ctx, types.NamespacedName{Namespace: key.Namespace, Name: secretName}, &secret)
|
||||||
|
switch {
|
||||||
|
case apierrors.IsNotFound(err):
|
||||||
|
if key.Spec.ImportExisting {
|
||||||
|
return r.fail(ctx, &key, "SecretMissing", fmt.Sprintf("import secret %s not found", secretName))
|
||||||
|
}
|
||||||
|
material, genErr := bind.GenerateSecret(bind.SecretBytesForAlgorithm(algorithm))
|
||||||
|
if genErr != nil {
|
||||||
|
return ctrl.Result{}, genErr
|
||||||
|
}
|
||||||
|
newSecret := &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: secretName, Namespace: key.Namespace, Labels: map[string]string{managedByLabel: managedByValue}},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"algorithm": []byte(algorithm),
|
||||||
|
"keyName": []byte(keyName),
|
||||||
|
"secret": []byte(material),
|
||||||
|
"key.conf": []byte(bind.KeyClause(keyName, algorithm, material)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := ctrl.SetControllerReference(&key, newSecret, r.Scheme); err != nil {
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
if err := r.Create(ctx, newSecret); err != nil {
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
logger.Info("generated TSIG key", "key", key.Name, "secret", secretName)
|
||||||
|
case err != nil:
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
key.Status.SecretName = secretName
|
||||||
|
key.Status.KeyName = keyName
|
||||||
|
key.Status.Ready = true
|
||||||
|
key.Status.ObservedGeneration = key.Generation
|
||||||
|
setReady(&key.Status.Conditions, key.Generation, true, "KeyReady", "TSIG key material present")
|
||||||
|
if err := r.Status().Update(ctx, &key); err != nil {
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BindTSIGKeyReconciler) fail(ctx context.Context, key *bindv1alpha1.BindTSIGKey, reason, msg string) (ctrl.Result, error) {
|
||||||
|
key.Status.Ready = false
|
||||||
|
key.Status.ObservedGeneration = key.Generation
|
||||||
|
setReady(&key.Status.Conditions, key.Generation, false, reason, msg)
|
||||||
|
if err := r.Status().Update(ctx, key); err != nil {
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
return ctrl.Result{RequeueAfter: requeueShort}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BindTSIGKeyReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||||
|
return ctrl.NewControllerManagedBy(mgr).
|
||||||
|
For(&bindv1alpha1.BindTSIGKey{}).
|
||||||
|
Owns(&corev1.Secret{}).
|
||||||
|
Complete(r)
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
|
||||||
|
bindv1alpha1 "git.unkin.net/unkin/bind-operator/api/v1alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BindViewReconciler validates a BindView and reports the number of zones bound
|
||||||
|
// to it. The view block is rendered into named.conf by the BindCluster
|
||||||
|
// controller, which watches views.
|
||||||
|
type BindViewReconciler struct {
|
||||||
|
client.Client
|
||||||
|
Scheme *runtime.Scheme
|
||||||
|
}
|
||||||
|
|
||||||
|
// +kubebuilder:rbac:groups=bind.unkin.net,resources=bindviews,verbs=get;list;watch;create;update;patch;delete
|
||||||
|
// +kubebuilder:rbac:groups=bind.unkin.net,resources=bindviews/status,verbs=get;update;patch
|
||||||
|
// +kubebuilder:rbac:groups=bind.unkin.net,resources=bindzones,verbs=get;list;watch
|
||||||
|
|
||||||
|
func (r *BindViewReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||||
|
var view bindv1alpha1.BindView
|
||||||
|
if err := r.Get(ctx, req.NamespacedName, &view); err != nil {
|
||||||
|
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var zones bindv1alpha1.BindZoneList
|
||||||
|
count := int32(0)
|
||||||
|
if err := r.List(ctx, &zones, client.InNamespace(view.Namespace)); err == nil {
|
||||||
|
for _, z := range zones.Items {
|
||||||
|
if z.Spec.ClusterRef == view.Spec.ClusterRef && z.Spec.ViewRef == view.Name {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
view.Status.ZoneCount = count
|
||||||
|
view.Status.Ready = view.Spec.ClusterRef != ""
|
||||||
|
view.Status.ObservedGeneration = view.Generation
|
||||||
|
setReady(&view.Status.Conditions, view.Generation, view.Status.Ready, "Validated", "view rendered into named.conf")
|
||||||
|
if err := r.Status().Update(ctx, &view); err != nil {
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BindViewReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||||
|
return ctrl.NewControllerManagedBy(mgr).
|
||||||
|
For(&bindv1alpha1.BindView{}).
|
||||||
|
Complete(r)
|
||||||
|
}
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||||
|
|
||||||
|
bindv1alpha1 "git.unkin.net/unkin/bind-operator/api/v1alpha1"
|
||||||
|
"git.unkin.net/unkin/bind-operator/internal/bind"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BindZoneReconciler provisions zones on a cluster primary via rndc addzone and
|
||||||
|
// seeds records via dynamic update.
|
||||||
|
type BindZoneReconciler struct {
|
||||||
|
client.Client
|
||||||
|
Scheme *runtime.Scheme
|
||||||
|
Exec *bind.Executor
|
||||||
|
}
|
||||||
|
|
||||||
|
// +kubebuilder:rbac:groups=bind.unkin.net,resources=bindzones,verbs=get;list;watch;create;update;patch;delete
|
||||||
|
// +kubebuilder:rbac:groups=bind.unkin.net,resources=bindzones/status,verbs=get;update;patch
|
||||||
|
// +kubebuilder:rbac:groups=bind.unkin.net,resources=bindcatalogzones;bindtsigkeys,verbs=get;list;watch
|
||||||
|
|
||||||
|
func (r *BindZoneReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||||
|
logger := log.FromContext(ctx)
|
||||||
|
|
||||||
|
var zone bindv1alpha1.BindZone
|
||||||
|
if err := r.Get(ctx, req.NamespacedName, &zone); err != nil {
|
||||||
|
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cluster, err := getCluster(ctx, r.Client, zone.Namespace, zone.Spec.ClusterRef)
|
||||||
|
if err != nil {
|
||||||
|
return r.setPhase(ctx, &zone, "Error", "ClusterMissing", err.Error())
|
||||||
|
}
|
||||||
|
primaryPod := primaryPodName(cluster.Name)
|
||||||
|
|
||||||
|
// Handle deletion via finalizer: remove the zone from the primary and catalog.
|
||||||
|
if !zone.DeletionTimestamp.IsZero() {
|
||||||
|
if controllerutil.ContainsFinalizer(&zone, finalizer) {
|
||||||
|
if primaryReady(ctx, r.Client, cluster) && r.Exec != nil {
|
||||||
|
_ = r.Exec.DelZone(ctx, zone.Namespace, primaryPod, zone.Spec.ZoneName, zone.Spec.ViewRef)
|
||||||
|
r.deregisterCatalog(ctx, &zone, cluster, primaryPod)
|
||||||
|
}
|
||||||
|
controllerutil.RemoveFinalizer(&zone, finalizer)
|
||||||
|
if err := r.Update(ctx, &zone); err != nil {
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !controllerutil.ContainsFinalizer(&zone, finalizer) {
|
||||||
|
controllerutil.AddFinalizer(&zone, finalizer)
|
||||||
|
if err := r.Update(ctx, &zone); err != nil {
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !primaryReady(ctx, r.Client, cluster) || r.Exec == nil {
|
||||||
|
return r.setPhase(ctx, &zone, "Pending", "PrimaryNotReady", "waiting for cluster primary to be ready")
|
||||||
|
}
|
||||||
|
|
||||||
|
zoneConfig, err := r.buildZoneConfig(ctx, &zone)
|
||||||
|
if err != nil {
|
||||||
|
return r.setPhase(ctx, &zone, "Error", "ConfigError", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
created := !r.Exec.ZoneExists(ctx, zone.Namespace, primaryPod, zone.Spec.ZoneName, zone.Spec.ViewRef)
|
||||||
|
if created && zone.Spec.Type == bindv1alpha1.ZonePrimary || (created && zone.Spec.Type == "") {
|
||||||
|
if err := r.Exec.WriteSeedZone(ctx, zone.Namespace, primaryPod, zone.Spec.ZoneName, bind.ZoneFilePath(zone.Spec.ZoneName), "", 1); err != nil {
|
||||||
|
return r.setPhase(ctx, &zone, "Error", "SeedFailed", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := r.Exec.AddZone(ctx, zone.Namespace, primaryPod, zone.Spec.ZoneName, zone.Spec.ViewRef, zoneConfig); err != nil {
|
||||||
|
return r.setPhase(ctx, &zone, "Error", "AddZoneFailed", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seed static records (primary zones only).
|
||||||
|
recordCount := 0
|
||||||
|
if isPrimaryType(zone.Spec.Type) && len(zone.Spec.Records) > 0 {
|
||||||
|
creds, err := r.zoneUpdateCreds(ctx, &zone)
|
||||||
|
if err != nil {
|
||||||
|
return r.setPhase(ctx, &zone, "Error", "NoUpdateKey", err.Error())
|
||||||
|
}
|
||||||
|
updates := recordsToUpdates(zone.Spec.ZoneName, zone.Spec.Records, zone.Spec.DefaultTTL)
|
||||||
|
if err := r.Exec.NSUpdate(ctx, zone.Namespace, primaryPod, zone.Spec.ZoneName, creds, updates); err != nil {
|
||||||
|
return r.setPhase(ctx, &zone, "Error", "RecordUpdateFailed", err.Error())
|
||||||
|
}
|
||||||
|
recordCount = len(updates)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register in the catalog so secondaries auto-provision.
|
||||||
|
if catalogEnabled(&zone) {
|
||||||
|
r.registerCatalog(ctx, &zone, cluster, primaryPod)
|
||||||
|
}
|
||||||
|
|
||||||
|
serial, _ := r.Exec.ZoneSerial(ctx, zone.Namespace, primaryPod, zone.Spec.ZoneName, zone.Spec.ViewRef)
|
||||||
|
zone.Status.Phase = "Ready"
|
||||||
|
zone.Status.Serial = serial
|
||||||
|
zone.Status.RecordCount = int32(recordCount)
|
||||||
|
zone.Status.Signed = zone.Spec.DNSSECPolicyRef != ""
|
||||||
|
zone.Status.ObservedGeneration = zone.Generation
|
||||||
|
setReady(&zone.Status.Conditions, zone.Generation, true, "Provisioned", "zone provisioned on primary")
|
||||||
|
if err := r.Status().Update(ctx, &zone); err != nil {
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
logger.Info("zone reconciled", "zone", zone.Spec.ZoneName, "serial", serial)
|
||||||
|
return ctrl.Result{RequeueAfter: requeueLong}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildZoneConfig renders the inner clause passed to rndc addzone/modzone.
|
||||||
|
func (r *BindZoneReconciler) buildZoneConfig(ctx context.Context, zone *bindv1alpha1.BindZone) (string, error) {
|
||||||
|
zType := zone.Spec.Type
|
||||||
|
if zType == "" {
|
||||||
|
zType = bindv1alpha1.ZonePrimary
|
||||||
|
}
|
||||||
|
var parts []string
|
||||||
|
switch zType {
|
||||||
|
case bindv1alpha1.ZonePrimary:
|
||||||
|
parts = append(parts, "type primary", fmt.Sprintf("file \"%s\"", bind.ZoneFilePath(zone.Spec.ZoneName)))
|
||||||
|
if zone.Spec.DynamicUpdate && zone.Spec.UpdateKeyRef != "" {
|
||||||
|
parts = append(parts, fmt.Sprintf("allow-update { key \"%s\"; }", updateKeyName(ctx, r.Client, zone)))
|
||||||
|
}
|
||||||
|
if len(zone.Spec.AllowTransfer) > 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("allow-transfer { %s }", matchListInline(zone.Spec.AllowTransfer)))
|
||||||
|
}
|
||||||
|
if zone.Spec.DNSSECPolicyRef != "" {
|
||||||
|
parts = append(parts, fmt.Sprintf("dnssec-policy \"%s\"", zone.Spec.DNSSECPolicyRef), "inline-signing yes")
|
||||||
|
}
|
||||||
|
case bindv1alpha1.ZoneSecondary:
|
||||||
|
parts = append(parts, "type secondary", fmt.Sprintf("file \"%s\"", bind.ZoneFilePath(zone.Spec.ZoneName)))
|
||||||
|
if len(zone.Spec.Primaries) > 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("primaries { %s }", terminateInline(zone.Spec.Primaries)))
|
||||||
|
}
|
||||||
|
case bindv1alpha1.ZoneForward:
|
||||||
|
parts = append(parts, "type forward", "forward only")
|
||||||
|
if len(zone.Spec.Forwarders) > 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("forwarders { %s }", terminateInline(zone.Spec.Forwarders)))
|
||||||
|
}
|
||||||
|
case bindv1alpha1.ZoneStub:
|
||||||
|
parts = append(parts, "type stub", fmt.Sprintf("file \"%s\"", bind.ZoneFilePath(zone.Spec.ZoneName)))
|
||||||
|
if len(zone.Spec.Primaries) > 0 {
|
||||||
|
parts = append(parts, fmt.Sprintf("primaries { %s }", terminateInline(zone.Spec.Primaries)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "{ " + strings.Join(parts, "; ") + "; };", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BindZoneReconciler) zoneUpdateCreds(ctx context.Context, zone *bindv1alpha1.BindZone) (bind.TSIGCreds, error) {
|
||||||
|
keyRef := zone.Spec.UpdateKeyRef
|
||||||
|
if keyRef == "" {
|
||||||
|
keyRef = zone.Spec.TransferKeyRef
|
||||||
|
}
|
||||||
|
if keyRef == "" {
|
||||||
|
// Fall back to local (non-TSIG) update when the zone allows it; most
|
||||||
|
// seeded primaries permit localhost updates.
|
||||||
|
return bind.TSIGCreds{}, fmt.Errorf("records require spec.updateKeyRef")
|
||||||
|
}
|
||||||
|
return resolveTSIG(ctx, r.Client, zone.Namespace, keyRef)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BindZoneReconciler) registerCatalog(ctx context.Context, zone *bindv1alpha1.BindZone, cluster *bindv1alpha1.BindCluster, primaryPod string) {
|
||||||
|
logger := log.FromContext(ctx)
|
||||||
|
catalog, creds, ok := r.catalogFor(ctx, zone, cluster)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := r.Exec.AddCatalogMember(ctx, zone.Namespace, primaryPod, catalog.Spec.ZoneName, zone.Spec.ZoneName, creds); err != nil {
|
||||||
|
logger.V(1).Info("catalog register failed", "zone", zone.Spec.ZoneName, "err", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BindZoneReconciler) deregisterCatalog(ctx context.Context, zone *bindv1alpha1.BindZone, cluster *bindv1alpha1.BindCluster, primaryPod string) {
|
||||||
|
catalog, creds, ok := r.catalogFor(ctx, zone, cluster)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = r.Exec.RemoveCatalogMember(ctx, zone.Namespace, primaryPod, catalog.Spec.ZoneName, zone.Spec.ZoneName, creds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BindZoneReconciler) catalogFor(ctx context.Context, zone *bindv1alpha1.BindZone, cluster *bindv1alpha1.BindCluster) (*bindv1alpha1.BindCatalogZone, bind.TSIGCreds, bool) {
|
||||||
|
var catalogs bindv1alpha1.BindCatalogZoneList
|
||||||
|
if err := r.List(ctx, &catalogs, client.InNamespace(zone.Namespace)); err != nil {
|
||||||
|
return nil, bind.TSIGCreds{}, false
|
||||||
|
}
|
||||||
|
for i := range catalogs.Items {
|
||||||
|
if catalogs.Items[i].Spec.ClusterRef == cluster.Name {
|
||||||
|
cat := &catalogs.Items[i]
|
||||||
|
creds, err := resolveTSIG(ctx, r.Client, zone.Namespace, cat.Spec.TransferKeyRef)
|
||||||
|
if err != nil {
|
||||||
|
return nil, bind.TSIGCreds{}, false
|
||||||
|
}
|
||||||
|
return cat, creds, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, bind.TSIGCreds{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BindZoneReconciler) setPhase(ctx context.Context, zone *bindv1alpha1.BindZone, phase, reason, msg string) (ctrl.Result, error) {
|
||||||
|
zone.Status.Phase = phase
|
||||||
|
zone.Status.ObservedGeneration = zone.Generation
|
||||||
|
setReady(&zone.Status.Conditions, zone.Generation, phase == "Ready", reason, msg)
|
||||||
|
if err := r.Status().Update(ctx, zone); err != nil {
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
if phase == "Error" || phase == "Pending" {
|
||||||
|
return ctrl.Result{RequeueAfter: requeueShort}, nil
|
||||||
|
}
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BindZoneReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||||
|
return ctrl.NewControllerManagedBy(mgr).
|
||||||
|
For(&bindv1alpha1.BindZone{}).
|
||||||
|
Complete(r)
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||||
|
|
||||||
|
bindv1alpha1 "git.unkin.net/unkin/bind-operator/api/v1alpha1"
|
||||||
|
"git.unkin.net/unkin/bind-operator/internal/bind"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DNSRecordReconciler applies individual record sets to a zone via TSIG dynamic
|
||||||
|
// update — the external-dns write path as a CRD.
|
||||||
|
type DNSRecordReconciler struct {
|
||||||
|
client.Client
|
||||||
|
Scheme *runtime.Scheme
|
||||||
|
Exec *bind.Executor
|
||||||
|
}
|
||||||
|
|
||||||
|
// +kubebuilder:rbac:groups=bind.unkin.net,resources=dnsrecords,verbs=get;list;watch;create;update;patch;delete
|
||||||
|
// +kubebuilder:rbac:groups=bind.unkin.net,resources=dnsrecords/status,verbs=get;update;patch
|
||||||
|
// +kubebuilder:rbac:groups=bind.unkin.net,resources=bindzones;bindtsigkeys,verbs=get;list;watch
|
||||||
|
|
||||||
|
func (r *DNSRecordReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||||
|
logger := log.FromContext(ctx)
|
||||||
|
|
||||||
|
var record bindv1alpha1.DNSRecord
|
||||||
|
if err := r.Get(ctx, req.NamespacedName, &record); err != nil {
|
||||||
|
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var zone bindv1alpha1.BindZone
|
||||||
|
if err := r.Get(ctx, client.ObjectKey{Namespace: record.Namespace, Name: record.Spec.ZoneRef}, &zone); err != nil {
|
||||||
|
return r.setPhase(ctx, &record, "Error", "ZoneMissing", err.Error())
|
||||||
|
}
|
||||||
|
cluster, err := getCluster(ctx, r.Client, record.Namespace, zone.Spec.ClusterRef)
|
||||||
|
if err != nil {
|
||||||
|
return r.setPhase(ctx, &record, "Error", "ClusterMissing", err.Error())
|
||||||
|
}
|
||||||
|
primaryPod := primaryPodName(cluster.Name)
|
||||||
|
name := fqdn(record.Spec.Name, zone.Spec.ZoneName)
|
||||||
|
|
||||||
|
creds, err := resolveTSIG(ctx, r.Client, record.Namespace, zone.Spec.UpdateKeyRef)
|
||||||
|
if err != nil {
|
||||||
|
return r.setPhase(ctx, &record, "Error", "NoUpdateKey", fmt.Sprintf("zone %s: %v", zone.Name, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deletion via finalizer: remove the RRset.
|
||||||
|
if !record.DeletionTimestamp.IsZero() {
|
||||||
|
if controllerutil.ContainsFinalizer(&record, finalizer) {
|
||||||
|
if primaryReady(ctx, r.Client, cluster) && r.Exec != nil {
|
||||||
|
_ = r.Exec.NSUpdate(ctx, record.Namespace, primaryPod, zone.Spec.ZoneName, creds,
|
||||||
|
[]bind.RecordUpdate{{FQDN: name, Type: record.Spec.Type, Delete: true}})
|
||||||
|
}
|
||||||
|
controllerutil.RemoveFinalizer(&record, finalizer)
|
||||||
|
if err := r.Update(ctx, &record); err != nil {
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !controllerutil.ContainsFinalizer(&record, finalizer) {
|
||||||
|
controllerutil.AddFinalizer(&record, finalizer)
|
||||||
|
if err := r.Update(ctx, &record); err != nil {
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !primaryReady(ctx, r.Client, cluster) || r.Exec == nil {
|
||||||
|
return r.setPhase(ctx, &record, "Pending", "PrimaryNotReady", "waiting for cluster primary")
|
||||||
|
}
|
||||||
|
|
||||||
|
ttl := zone.Spec.DefaultTTL
|
||||||
|
if record.Spec.TTL != nil {
|
||||||
|
ttl = *record.Spec.TTL
|
||||||
|
}
|
||||||
|
update := bind.RecordUpdate{FQDN: name, Type: record.Spec.Type, TTL: ttl, Values: record.Spec.Values}
|
||||||
|
if err := r.Exec.NSUpdate(ctx, record.Namespace, primaryPod, zone.Spec.ZoneName, creds, []bind.RecordUpdate{update}); err != nil {
|
||||||
|
return r.setPhase(ctx, &record, "Error", "UpdateFailed", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
record.Status.FQDN = name
|
||||||
|
record.Status.Phase = "Applied"
|
||||||
|
record.Status.ObservedGeneration = record.Generation
|
||||||
|
setReady(&record.Status.Conditions, record.Generation, true, "Applied", "record applied via dynamic update")
|
||||||
|
if err := r.Status().Update(ctx, &record); err != nil {
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
logger.Info("record applied", "record", name, "type", record.Spec.Type)
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DNSRecordReconciler) setPhase(ctx context.Context, record *bindv1alpha1.DNSRecord, phase, reason, msg string) (ctrl.Result, error) {
|
||||||
|
record.Status.Phase = phase
|
||||||
|
record.Status.ObservedGeneration = record.Generation
|
||||||
|
setReady(&record.Status.Conditions, record.Generation, phase == "Applied", reason, msg)
|
||||||
|
if err := r.Status().Update(ctx, record); err != nil {
|
||||||
|
return ctrl.Result{}, err
|
||||||
|
}
|
||||||
|
if phase == "Error" || phase == "Pending" {
|
||||||
|
return ctrl.Result{RequeueAfter: requeueShort}, nil
|
||||||
|
}
|
||||||
|
return ctrl.Result{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DNSRecordReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||||
|
return ctrl.NewControllerManagedBy(mgr).
|
||||||
|
For(&bindv1alpha1.DNSRecord{}).
|
||||||
|
Complete(r)
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
func headlessServiceName(cluster string) string { return cluster + "-headless" }
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
|
||||||
|
"git.unkin.net/unkin/bind-operator/internal/bind"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetupAll registers every controller with the manager.
|
||||||
|
func SetupAll(mgr ctrl.Manager, exec *bind.Executor) error {
|
||||||
|
if err := (&BindClusterReconciler{Client: mgr.GetClient(), Scheme: mgr.GetScheme(), Exec: exec}).SetupWithManager(mgr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := (&BindTSIGKeyReconciler{Client: mgr.GetClient(), Scheme: mgr.GetScheme()}).SetupWithManager(mgr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := (&BindACLReconciler{Client: mgr.GetClient(), Scheme: mgr.GetScheme()}).SetupWithManager(mgr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := (&BindViewReconciler{Client: mgr.GetClient(), Scheme: mgr.GetScheme()}).SetupWithManager(mgr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := (&BindDNSSECPolicyReconciler{Client: mgr.GetClient(), Scheme: mgr.GetScheme()}).SetupWithManager(mgr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := (&BindCatalogZoneReconciler{Client: mgr.GetClient(), Scheme: mgr.GetScheme(), Exec: exec}).SetupWithManager(mgr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := (&BindZoneReconciler{Client: mgr.GetClient(), Scheme: mgr.GetScheme(), Exec: exec}).SetupWithManager(mgr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := (&BindPolicyReconciler{Client: mgr.GetClient(), Scheme: mgr.GetScheme(), Exec: exec}).SetupWithManager(mgr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := (&DNSRecordReconciler{Client: mgr.GetClient(), Scheme: mgr.GetScheme(), Exec: exec}).SetupWithManager(mgr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"k8s.io/apimachinery/pkg/util/intstr"
|
||||||
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
|
||||||
|
bindv1alpha1 "git.unkin.net/unkin/bind-operator/api/v1alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func intstrFromInt(i int) intstr.IntOrString { return intstr.FromInt(i) }
|
||||||
|
|
||||||
|
func podReady(pod *corev1.Pod) bool {
|
||||||
|
for _, c := range pod.Status.Conditions {
|
||||||
|
if c.Type == corev1.PodReady {
|
||||||
|
return c.Status == corev1.ConditionTrue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
set -eu
|
||||||
|
ORD="${HOSTNAME##*-}"
|
||||||
|
if [ "$ORD" = "0" ]; then
|
||||||
|
cp /etc/bind/named.conf.primary /run/named/named.conf
|
||||||
|
else
|
||||||
|
cp /etc/bind/named.conf.secondary /run/named/named.conf
|
||||||
|
fi
|
||||||
|
mkdir -p /var/lib/named/zones /var/lib/named/catalog
|
||||||
|
exec named -g -c /run/named/named.conf
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BindClusterReconciler) upsertService(ctx context.Context, c *bindv1alpha1.BindCluster, desired *corev1.Service) error {
|
||||||
|
if err := ctrl.SetControllerReference(c, desired, r.Scheme); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var existing corev1.Service
|
||||||
|
err := r.Get(ctx, types.NamespacedName{Namespace: desired.Namespace, Name: desired.Name}, &existing)
|
||||||
|
if apierrors.IsNotFound(err) {
|
||||||
|
return r.Create(ctx, desired)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
existing.Spec.Ports = desired.Spec.Ports
|
||||||
|
existing.Spec.Selector = desired.Spec.Selector
|
||||||
|
existing.Spec.Type = desired.Spec.Type
|
||||||
|
existing.Spec.LoadBalancerIP = desired.Spec.LoadBalancerIP
|
||||||
|
if desired.Annotations != nil {
|
||||||
|
if existing.Annotations == nil {
|
||||||
|
existing.Annotations = map[string]string{}
|
||||||
|
}
|
||||||
|
for k, v := range desired.Annotations {
|
||||||
|
existing.Annotations[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r.Update(ctx, &existing)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BindClusterReconciler) upsertConfigMap(ctx context.Context, c *bindv1alpha1.BindCluster, name string, data map[string]string) error {
|
||||||
|
desired := &corev1.ConfigMap{}
|
||||||
|
desired.Name = name
|
||||||
|
desired.Namespace = c.Namespace
|
||||||
|
desired.Labels = commonLabels(c.Name)
|
||||||
|
desired.Data = data
|
||||||
|
if err := ctrl.SetControllerReference(c, desired, r.Scheme); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var existing corev1.ConfigMap
|
||||||
|
err := r.Get(ctx, types.NamespacedName{Namespace: c.Namespace, Name: name}, &existing)
|
||||||
|
if apierrors.IsNotFound(err) {
|
||||||
|
return r.Create(ctx, desired)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
existing.Data = data
|
||||||
|
existing.Labels = commonLabels(c.Name)
|
||||||
|
return r.Update(ctx, &existing)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BindClusterReconciler) upsertSecret(ctx context.Context, c *bindv1alpha1.BindCluster, name string, data map[string][]byte) error {
|
||||||
|
desired := &corev1.Secret{}
|
||||||
|
desired.Name = name
|
||||||
|
desired.Namespace = c.Namespace
|
||||||
|
desired.Labels = commonLabels(c.Name)
|
||||||
|
desired.Data = data
|
||||||
|
if err := ctrl.SetControllerReference(c, desired, r.Scheme); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var existing corev1.Secret
|
||||||
|
err := r.Get(ctx, types.NamespacedName{Namespace: c.Namespace, Name: name}, &existing)
|
||||||
|
if apierrors.IsNotFound(err) {
|
||||||
|
return r.Create(ctx, desired)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
existing.Data = data
|
||||||
|
existing.Labels = commonLabels(c.Name)
|
||||||
|
return r.Update(ctx, &existing)
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
|
||||||
|
bindv1alpha1 "git.unkin.net/unkin/bind-operator/api/v1alpha1"
|
||||||
|
"git.unkin.net/unkin/bind-operator/internal/bind"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isPrimaryType(t bindv1alpha1.ZoneType) bool {
|
||||||
|
return t == bindv1alpha1.ZonePrimary || t == ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// catalogEnabled reports whether a primary zone should be registered in the
|
||||||
|
// cluster catalog zone.
|
||||||
|
func catalogEnabled(zone *bindv1alpha1.BindZone) bool {
|
||||||
|
if !isPrimaryType(zone.Spec.Type) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if zone.Spec.Catalog == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return *zone.Spec.Catalog
|
||||||
|
}
|
||||||
|
|
||||||
|
// fqdn resolves a record owner name relative to a zone origin.
|
||||||
|
func fqdn(name, zone string) string {
|
||||||
|
zone = strings.TrimSuffix(zone, ".") + "."
|
||||||
|
if name == "" || name == "@" {
|
||||||
|
return zone
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(name, ".") {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
return name + "." + zone
|
||||||
|
}
|
||||||
|
|
||||||
|
func recordsToUpdates(zone string, records []bindv1alpha1.Record, defaultTTL int32) []bind.RecordUpdate {
|
||||||
|
updates := make([]bind.RecordUpdate, 0, len(records))
|
||||||
|
for _, rec := range records {
|
||||||
|
ttl := defaultTTL
|
||||||
|
if rec.TTL != nil {
|
||||||
|
ttl = *rec.TTL
|
||||||
|
}
|
||||||
|
updates = append(updates, bind.RecordUpdate{
|
||||||
|
FQDN: fqdn(rec.Name, zone),
|
||||||
|
Type: rec.Type,
|
||||||
|
TTL: ttl,
|
||||||
|
Values: rec.Values,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return updates
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateKeyName returns the TSIG key name (as used in named.conf) for a zone's
|
||||||
|
// update key, falling back to the object name.
|
||||||
|
func updateKeyName(ctx context.Context, c client.Client, zone *bindv1alpha1.BindZone) string {
|
||||||
|
ref := zone.Spec.UpdateKeyRef
|
||||||
|
if ref == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var key bindv1alpha1.BindTSIGKey
|
||||||
|
if err := c.Get(ctx, client.ObjectKey{Namespace: zone.Namespace, Name: ref}, &key); err != nil {
|
||||||
|
return ref
|
||||||
|
}
|
||||||
|
if key.Spec.KeyName != "" {
|
||||||
|
return key.Spec.KeyName
|
||||||
|
}
|
||||||
|
return ref
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchListInline renders address-match-list entries on one line.
|
||||||
|
func matchListInline(entries []string) string { return terminateInline(entries) }
|
||||||
|
|
||||||
|
func terminateInline(entries []string) string {
|
||||||
|
var parts []string
|
||||||
|
for _, e := range entries {
|
||||||
|
e = strings.TrimSpace(strings.TrimRight(e, ";"))
|
||||||
|
if e == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts = append(parts, e+";")
|
||||||
|
}
|
||||||
|
return strings.Join(parts, " ")
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
bindv1alpha1 "git.unkin.net/unkin/bind-operator/api/v1alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFQDN(t *testing.T) {
|
||||||
|
cases := []struct{ name, zone, want string }{
|
||||||
|
{"@", "example.com", "example.com."},
|
||||||
|
{"", "example.com", "example.com."},
|
||||||
|
{"www", "example.com", "www.example.com."},
|
||||||
|
{"www.example.com.", "example.com", "www.example.com."},
|
||||||
|
{"host", "10.in-addr.arpa", "host.10.in-addr.arpa."},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
if got := fqdn(c.name, c.zone); got != c.want {
|
||||||
|
t.Errorf("fqdn(%q,%q)=%q want %q", c.name, c.zone, got, c.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRecordsToUpdatesTTLFallback(t *testing.T) {
|
||||||
|
custom := int32(60)
|
||||||
|
records := []bindv1alpha1.Record{
|
||||||
|
{Name: "@", Type: "A", Values: []string{"192.0.2.1"}},
|
||||||
|
{Name: "low", Type: "A", TTL: &custom, Values: []string{"192.0.2.2"}},
|
||||||
|
}
|
||||||
|
updates := recordsToUpdates("example.com", records, 3600)
|
||||||
|
if len(updates) != 2 {
|
||||||
|
t.Fatalf("expected 2 updates, got %d", len(updates))
|
||||||
|
}
|
||||||
|
if updates[0].TTL != 3600 {
|
||||||
|
t.Errorf("expected default TTL 3600, got %d", updates[0].TTL)
|
||||||
|
}
|
||||||
|
if updates[1].TTL != 60 {
|
||||||
|
t.Errorf("expected record TTL 60, got %d", updates[1].TTL)
|
||||||
|
}
|
||||||
|
if updates[0].FQDN != "example.com." {
|
||||||
|
t.Errorf("apex FQDN wrong: %s", updates[0].FQDN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRPZRulesToUpdates(t *testing.T) {
|
||||||
|
rules := []bindv1alpha1.RPZRule{
|
||||||
|
{Trigger: "qname", Match: "bad.example.com", Action: "nxdomain"},
|
||||||
|
{Trigger: "qname", Match: "walled.example.com", Action: "cname", Target: "block.internal"},
|
||||||
|
}
|
||||||
|
updates := rpzRulesToUpdates("rpz.internal", rules)
|
||||||
|
if len(updates) != 2 {
|
||||||
|
t.Fatalf("expected 2 updates, got %d", len(updates))
|
||||||
|
}
|
||||||
|
if updates[0].FQDN != "bad.example.com.rpz.internal." {
|
||||||
|
t.Errorf("qname owner wrong: %s", updates[0].FQDN)
|
||||||
|
}
|
||||||
|
if updates[0].Values[0] != "." {
|
||||||
|
t.Errorf("nxdomain rdata should be '.', got %q", updates[0].Values[0])
|
||||||
|
}
|
||||||
|
if updates[1].Values[0] != "block.internal." {
|
||||||
|
t.Errorf("cname rdata wrong: %q", updates[1].Values[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCatalogEnabledDefault(t *testing.T) {
|
||||||
|
on := &bindv1alpha1.BindZone{Spec: bindv1alpha1.BindZoneSpec{Type: bindv1alpha1.ZonePrimary}}
|
||||||
|
if !catalogEnabled(on) {
|
||||||
|
t.Error("primary zone should default to catalog enabled")
|
||||||
|
}
|
||||||
|
no := false
|
||||||
|
off := &bindv1alpha1.BindZone{Spec: bindv1alpha1.BindZoneSpec{Type: bindv1alpha1.ZonePrimary, Catalog: &no}}
|
||||||
|
if catalogEnabled(off) {
|
||||||
|
t.Error("catalog=false should disable membership")
|
||||||
|
}
|
||||||
|
sec := &bindv1alpha1.BindZone{Spec: bindv1alpha1.BindZoneSpec{Type: bindv1alpha1.ZoneSecondary}}
|
||||||
|
if catalogEnabled(sec) {
|
||||||
|
t.Error("secondary zone should never be a catalog member")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user