diff --git a/api/v1/resolution_types.go b/api/v1/resolution_types.go index 432f476..5542663 100644 --- a/api/v1/resolution_types.go +++ b/api/v1/resolution_types.go @@ -35,6 +35,10 @@ type ResolutionSpec struct { // Address to be queried for IP // +required 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. @@ -63,11 +67,16 @@ type ResolutionStatus struct { // +kubebuilder:title=`IP Addresses` // +optional IPAddresses []net.IP `json:"ipAddresses,omitempty"` + + // Resolver used for this lookup + // +optional + Resolver *string `json:"resolver,omitempty"` } // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +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` // Resolution is the Schema for the resolutions API diff --git a/api/v1/resolver_types.go b/api/v1/resolver_types.go index dceeb09..23bbfd6 100644 --- a/api/v1/resolver_types.go +++ b/api/v1/resolver_types.go @@ -35,6 +35,11 @@ type ResolverSpec struct { // IPAddress is the main field for a resolver. This will be used to run queries against. // +required 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. diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index 2eeafe1..44fd72c 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -31,7 +31,7 @@ func (in *Resolution) DeepCopyInto(out *Resolution) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) 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. func (in *ResolutionSpec) DeepCopyInto(out *ResolutionSpec) { *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. @@ -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. diff --git a/config/crd/bases/dns.stinnesbeck.com_resolutions.yaml b/config/crd/bases/dns.stinnesbeck.com_resolutions.yaml index 6c37a86..3ecd8d7 100644 --- a/config/crd/bases/dns.stinnesbeck.com_resolutions.yaml +++ b/config/crd/bases/dns.stinnesbeck.com_resolutions.yaml @@ -18,6 +18,9 @@ spec: - jsonPath: .spec.address name: Address type: string + - jsonPath: .spec.resolver + name: Resolver + type: string - jsonPath: .status.ipAddresses name: IPs type: string @@ -49,6 +52,10 @@ spec: address: description: Address to be queried for IP type: string + resolver: + description: Resolver can be used to specify another resolver instead + of the hosts default resolver + type: string required: - address type: object @@ -131,6 +138,9 @@ spec: type: string title: IP Addresses type: array + resolver: + description: Resolver used for this lookup + type: string type: object required: - spec diff --git a/config/crd/bases/dns.stinnesbeck.com_resolvers.yaml b/config/crd/bases/dns.stinnesbeck.com_resolvers.yaml index 5cefc7d..4fac503 100644 --- a/config/crd/bases/dns.stinnesbeck.com_resolvers.yaml +++ b/config/crd/bases/dns.stinnesbeck.com_resolvers.yaml @@ -43,6 +43,11 @@ spec: description: IPAddress is the main field for a resolver. This will be used to run queries against. type: string + port: + default: 53 + description: Port used for dns lookup queries, will default to 53 + if omitted + type: integer required: - ipAddress type: object diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index 78de894..b0aa6ee 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -5,4 +5,4 @@ kind: Kustomization images: - name: controller newName: git.stinnesbeck.com/k8s/dns-operator - newTag: v0.0.1 + newTag: v0.0.22 diff --git a/config/samples/dns_v1_resolution.yaml b/config/samples/dns_v1_resolution.yaml index 3674c45..1e1f5c6 100644 --- a/config/samples/dns_v1_resolution.yaml +++ b/config/samples/dns_v1_resolution.yaml @@ -28,3 +28,4 @@ metadata: name: whatismyip spec: address: whatismyip.com + resolver: fw diff --git a/config/samples/dns_v1_resolver.yaml b/config/samples/dns_v1_resolver.yaml index fe4d433..a084733 100644 --- a/config/samples/dns_v1_resolver.yaml +++ b/config/samples/dns_v1_resolver.yaml @@ -8,3 +8,13 @@ metadata: name: cloudflare spec: 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 diff --git a/internal/controller/resolution_controller.go b/internal/controller/resolution_controller.go index 36b5ce5..45ca455 100644 --- a/internal/controller/resolution_controller.go +++ b/internal/controller/resolution_controller.go @@ -17,11 +17,11 @@ limitations under the License. package controller import ( - "bytes" "context" "fmt" "net" "slices" + "time" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" @@ -30,6 +30,7 @@ import ( logf "sigs.k8s.io/controller-runtime/pkg/log" dnsv1 "stinnesbeck.com/dns/api/v1" + v1 "stinnesbeck.com/dns/api/v1" ) // ResolutionReconciler reconciles a Resolution object @@ -68,16 +69,31 @@ func (r *ResolutionReconciler) Reconcile(ctx context.Context, req ctrl.Request) 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 patchBase := resolution.DeepCopy() - addrs, err := net.LookupHost(resolution.Spec.Address) + var addrs []string + + addrs, err := resolver.LookupHost(ctx, resolution.Spec.Address) if err != nil { log.Error(err, "error while resolving resolution") return ctrl.Result{}, err } - // Build new desired status (IMPORTANT: no append) + // Build new desired status var ips []net.IP for _, addr := range addrs { 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 { - for i := range 15 { - if a[i] < b[i] { - return -1 - } - - if a[i] > b[i] { - return 1 - } - } - - return 0 - }) + // sort to make states comparable + slices.SortStableFunc(ips, stableSortIPs) // check if both ip address slices are the same - if slices.EqualFunc(patchBase.Status.IPAddresses, ips, func(s1, s2 net.IP) bool { - return bytes.Equal(s1, s2) - }) { + if slices.EqualFunc(patchBase.Status.IPAddresses, ips, equalIPs) { return ctrl.Result{}, nil } @@ -111,30 +114,70 @@ func (r *ResolutionReconciler) Reconcile(ctx context.Context, req ctrl.Request) resolution.Status.IPAddresses = ips // Only patch status (clean + minimal diff) - if err := r.Status().Patch( - ctx, - &resolution, - client.MergeFrom(patchBase), - ); err != nil { + if err := r.Status().Patch(ctx, &resolution, client.MergeFrom(patchBase)); err != nil { return ctrl.Result{}, err } 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 // } - // r := &net.Resolver{ - // PreferGo: true, - // Dial: func(ctx context.Context, network, address string) (net.Conn, error) { - // d := net.Dialer{ - // Timeout: time.Millisecond * time.Duration(10000), - // } - // return d.DialContext(ctx, network, "8.8.8.8:53") - // }, - // } - // ip, _ := r.LookupHost(context.Background(), "www.google.com") + // return ctrl.Result{}, nil +} + +func equalIPs(a, b net.IP) bool { + return a.Equal(b) +} + +func stableSortIPs(a, b net.IP) int { + if len(a) != len(b) { + 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. diff --git a/internal/controller/resolver_controller.go b/internal/controller/resolver_controller.go index caf0ae8..9edfdd3 100644 --- a/internal/controller/resolver_controller.go +++ b/internal/controller/resolver_controller.go @@ -69,7 +69,6 @@ func (r *ResolverReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c return ctrl.Result{}, err } - // TODO(user): your logic here switch { case len(resolver.Spec.IPAddress) == 0: // IPAddress is not a valid IP @@ -81,6 +80,17 @@ func (r *ResolverReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c // 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 }