Compare commits

1 Commits
v0.0.1 ... dev

Author SHA1 Message Date
d7c9e217be more changes
Some checks failed
Lint / Run on Ubuntu (push) Has been cancelled
E2E Tests / Run on Ubuntu (push) Has been cancelled
Tests / Run on Ubuntu (push) Has been cancelled
2026-04-14 07:40:25 +02:00
10 changed files with 141 additions and 38 deletions

View File

@@ -35,6 +35,10 @@ type ResolutionSpec struct {
// Address to be queried for IP // Address to be queried for IP
// +required // +required
Address string `json:"address"` Address string `json:"address"`
// Resolver can be used to specify another resolver instead of the hosts default resolver
// +optional
Resolver *string `json:"resolver,omitempty"`
} }
// ResolutionStatus defines the observed state of Resolution. // ResolutionStatus defines the observed state of Resolution.
@@ -63,11 +67,16 @@ type ResolutionStatus struct {
// +kubebuilder:title=`IP Addresses` // +kubebuilder:title=`IP Addresses`
// +optional // +optional
IPAddresses []net.IP `json:"ipAddresses,omitempty"` IPAddresses []net.IP `json:"ipAddresses,omitempty"`
// Resolver used for this lookup
// +optional
Resolver *string `json:"resolver,omitempty"`
} }
// +kubebuilder:object:root=true // +kubebuilder:object:root=true
// +kubebuilder:subresource:status // +kubebuilder:subresource:status
// +kubebuilder:printcolumn:name="Address",type=string,JSONPath=`.spec.address` // +kubebuilder:printcolumn:name="Address",type=string,JSONPath=`.spec.address`
// +kubebuilder:printcolumn:name="Resolver",type=string,JSONPath=`.spec.resolver`
// +kubebuilder:printcolumn:name="IPs",type=string,JSONPath=`.status.ipAddresses` // +kubebuilder:printcolumn:name="IPs",type=string,JSONPath=`.status.ipAddresses`
// Resolution is the Schema for the resolutions API // Resolution is the Schema for the resolutions API

View File

@@ -35,6 +35,11 @@ type ResolverSpec struct {
// IPAddress is the main field for a resolver. This will be used to run queries against. // IPAddress is the main field for a resolver. This will be used to run queries against.
// +required // +required
IPAddress net.IP `json:"ipAddress"` IPAddress net.IP `json:"ipAddress"`
// Port used for dns lookup queries, will default to 53 if omitted
// +kubebuilder:default=53
// +optional
Port uint `json:"port"`
} }
// ResolverStatus defines the observed state of Resolver. // ResolverStatus defines the observed state of Resolver.

View File

@@ -31,7 +31,7 @@ func (in *Resolution) DeepCopyInto(out *Resolution) {
*out = *in *out = *in
out.TypeMeta = in.TypeMeta out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
out.Spec = in.Spec in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status) in.Status.DeepCopyInto(&out.Status)
} }
@@ -88,6 +88,11 @@ func (in *ResolutionList) DeepCopyObject() runtime.Object {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ResolutionSpec) DeepCopyInto(out *ResolutionSpec) { func (in *ResolutionSpec) DeepCopyInto(out *ResolutionSpec) {
*out = *in *out = *in
if in.Resolver != nil {
in, out := &in.Resolver, &out.Resolver
*out = new(string)
**out = **in
}
} }
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResolutionSpec. // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResolutionSpec.
@@ -121,6 +126,11 @@ func (in *ResolutionStatus) DeepCopyInto(out *ResolutionStatus) {
} }
} }
} }
if in.Resolver != nil {
in, out := &in.Resolver, &out.Resolver
*out = new(string)
**out = **in
}
} }
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResolutionStatus. // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResolutionStatus.

View File

@@ -18,6 +18,9 @@ spec:
- jsonPath: .spec.address - jsonPath: .spec.address
name: Address name: Address
type: string type: string
- jsonPath: .spec.resolver
name: Resolver
type: string
- jsonPath: .status.ipAddresses - jsonPath: .status.ipAddresses
name: IPs name: IPs
type: string type: string
@@ -49,6 +52,10 @@ spec:
address: address:
description: Address to be queried for IP description: Address to be queried for IP
type: string type: string
resolver:
description: Resolver can be used to specify another resolver instead
of the hosts default resolver
type: string
required: required:
- address - address
type: object type: object
@@ -131,6 +138,9 @@ spec:
type: string type: string
title: IP Addresses title: IP Addresses
type: array type: array
resolver:
description: Resolver used for this lookup
type: string
type: object type: object
required: required:
- spec - spec

View File

@@ -43,6 +43,11 @@ spec:
description: IPAddress is the main field for a resolver. This will description: IPAddress is the main field for a resolver. This will
be used to run queries against. be used to run queries against.
type: string type: string
port:
default: 53
description: Port used for dns lookup queries, will default to 53
if omitted
type: integer
required: required:
- ipAddress - ipAddress
type: object type: object

View File

@@ -5,4 +5,4 @@ kind: Kustomization
images: images:
- name: controller - name: controller
newName: git.stinnesbeck.com/k8s/dns-operator newName: git.stinnesbeck.com/k8s/dns-operator
newTag: v0.0.1 newTag: v0.0.22

View File

@@ -28,3 +28,4 @@ metadata:
name: whatismyip name: whatismyip
spec: spec:
address: whatismyip.com address: whatismyip.com
resolver: fw

View File

@@ -8,3 +8,13 @@ metadata:
name: cloudflare name: cloudflare
spec: spec:
ipAddress: 1.1.1.1 ipAddress: 1.1.1.1
---
apiVersion: dns.stinnesbeck.com/v1
kind: Resolver
metadata:
labels:
app.kubernetes.io/name: dns-operator
app.kubernetes.io/managed-by: kustomize
name: fw
spec:
ipAddress: 192.168.168.254

View File

@@ -17,11 +17,11 @@ limitations under the License.
package controller package controller
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"net" "net"
"slices" "slices"
"time"
apierrors "k8s.io/apimachinery/pkg/api/errors" apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
@@ -30,6 +30,7 @@ import (
logf "sigs.k8s.io/controller-runtime/pkg/log" logf "sigs.k8s.io/controller-runtime/pkg/log"
dnsv1 "stinnesbeck.com/dns/api/v1" dnsv1 "stinnesbeck.com/dns/api/v1"
v1 "stinnesbeck.com/dns/api/v1"
) )
// ResolutionReconciler reconciles a Resolution object // ResolutionReconciler reconciles a Resolution object
@@ -68,16 +69,31 @@ func (r *ResolutionReconciler) Reconcile(ctx context.Context, req ctrl.Request)
return ctrl.Result{}, err return ctrl.Result{}, err
} }
resolver := net.DefaultResolver
// check if a resolver was given
if resolution.Spec.Resolver != nil {
customResolver, err := r.getResolver(ctx, req, &resolution)
if err != nil {
return ctrl.Result{}, err
}
resolution.Status.Resolver = resolution.Spec.Resolver
resolver = customResolver
}
// Snapshot before mutation // Snapshot before mutation
patchBase := resolution.DeepCopy() patchBase := resolution.DeepCopy()
addrs, err := net.LookupHost(resolution.Spec.Address) var addrs []string
addrs, err := resolver.LookupHost(ctx, resolution.Spec.Address)
if err != nil { if err != nil {
log.Error(err, "error while resolving resolution") log.Error(err, "error while resolving resolution")
return ctrl.Result{}, err return ctrl.Result{}, err
} }
// Build new desired status (IMPORTANT: no append) // Build new desired status
var ips []net.IP var ips []net.IP
for _, addr := range addrs { for _, addr := range addrs {
if ip := net.ParseIP(addr); ip != nil { if ip := net.ParseIP(addr); ip != nil {
@@ -85,24 +101,11 @@ func (r *ResolutionReconciler) Reconcile(ctx context.Context, req ctrl.Request)
} }
} }
slices.SortStableFunc(ips, func(a, b net.IP) int { // sort to make states comparable
for i := range 15 { slices.SortStableFunc(ips, stableSortIPs)
if a[i] < b[i] {
return -1
}
if a[i] > b[i] {
return 1
}
}
return 0
})
// check if both ip address slices are the same // check if both ip address slices are the same
if slices.EqualFunc(patchBase.Status.IPAddresses, ips, func(s1, s2 net.IP) bool { if slices.EqualFunc(patchBase.Status.IPAddresses, ips, equalIPs) {
return bytes.Equal(s1, s2)
}) {
return ctrl.Result{}, nil return ctrl.Result{}, nil
} }
@@ -111,30 +114,70 @@ func (r *ResolutionReconciler) Reconcile(ctx context.Context, req ctrl.Request)
resolution.Status.IPAddresses = ips resolution.Status.IPAddresses = ips
// Only patch status (clean + minimal diff) // Only patch status (clean + minimal diff)
if err := r.Status().Patch( if err := r.Status().Patch(ctx, &resolution, client.MergeFrom(patchBase)); err != nil {
ctx,
&resolution,
client.MergeFrom(patchBase),
); err != nil {
return ctrl.Result{}, err return ctrl.Result{}, err
} }
return ctrl.Result{}, nil return ctrl.Result{}, nil
}
// if err := r.Patch(ctx, &resolution, client.Merge); err != nil { func (r *ResolutionReconciler) getResolver(ctx context.Context, req ctrl.Request, resolution *v1.Resolution) (*net.Resolver, error) {
var resolver v1.Resolver
resolverFQDN := client.ObjectKey{
Namespace: req.Namespace,
Name: *resolution.Spec.Resolver,
}
if err := r.Get(ctx, resolverFQDN, &resolver); err != nil {
// retrieval of resolver failed
return nil, err
}
netResolver := net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{
Timeout: 3 * time.Second,
}
return d.DialContext(ctx, network, fmt.Sprintf("%s:%d", resolver.Spec.IPAddress, resolver.Spec.Port))
},
}
return &netResolver, nil
// // Snapshot before mutation
// patchBase := resolution.DeepCopy()
// ip, _ := netResolver.LookupHost(context.Background(), resolution.Spec.Address)
// if err := r.Status().Patch(ctx, resolution, client.MergeFrom(patchBase)); err != nil {
// return ctrl.Result{}, err // return ctrl.Result{}, err
// } // }
// r := &net.Resolver{ // return ctrl.Result{}, nil
// PreferGo: true, }
// Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
// d := net.Dialer{ func equalIPs(a, b net.IP) bool {
// Timeout: time.Millisecond * time.Duration(10000), return a.Equal(b)
// } }
// return d.DialContext(ctx, network, "8.8.8.8:53")
// }, func stableSortIPs(a, b net.IP) int {
// } if len(a) != len(b) {
// ip, _ := r.LookupHost(context.Background(), "www.google.com") a, b = a.To16(), b.To16()
}
for i := range a {
if a[i] < b[i] {
return -1
}
if a[i] > b[i] {
return 1
}
}
return 0
} }
// SetupWithManager sets up the controller with the Manager. // SetupWithManager sets up the controller with the Manager.

View File

@@ -69,7 +69,6 @@ func (r *ResolverReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
return ctrl.Result{}, err return ctrl.Result{}, err
} }
// TODO(user): your logic here
switch { switch {
case len(resolver.Spec.IPAddress) == 0: case len(resolver.Spec.IPAddress) == 0:
// IPAddress is not a valid IP // IPAddress is not a valid IP
@@ -81,6 +80,17 @@ func (r *ResolverReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
// IP is IPv6 // IP is IPv6
} }
// exit loop here if port was provided
if resolver.Spec.Port != 0 {
return ctrl.Result{}, nil
}
// set defaults and patch it back to k8s
resolver.Spec.Port = 53
if err := r.Patch(ctx, &resolver, client.Merge); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil return ctrl.Result{}, nil
} }