From 58952245a0a5c1b50bc39d7e9d3f9529758b6346 Mon Sep 17 00:00:00 2001 From: immanuwell Date: Thu, 21 May 2026 22:28:40 +0400 Subject: [PATCH] fix: handle unresolved dev DNS updater hostnames Signed-off-by: immanuwell --- src/utils/dev_dns_updater/BUILD.bazel | 13 +- src/utils/dev_dns_updater/dev_dns_updater.go | 32 +++-- .../dev_dns_updater/dev_dns_updater_test.go | 114 ++++++++++++++++++ 3 files changed, 150 insertions(+), 9 deletions(-) create mode 100644 src/utils/dev_dns_updater/dev_dns_updater_test.go diff --git a/src/utils/dev_dns_updater/BUILD.bazel b/src/utils/dev_dns_updater/BUILD.bazel index 0fa6a27183e..58ff1a42ff3 100644 --- a/src/utils/dev_dns_updater/BUILD.bazel +++ b/src/utils/dev_dns_updater/BUILD.bazel @@ -15,7 +15,7 @@ # SPDX-License-Identifier: Apache-2.0 load("@io_bazel_rules_go//go:def.bzl", "go_library") -load("//bazel:pl_build_system.bzl", "pl_go_binary") +load("//bazel:pl_build_system.bzl", "pl_go_binary", "pl_go_test") go_library( name = "dev_dns_updater_lib", @@ -44,3 +44,14 @@ pl_go_binary( embed = [":dev_dns_updater_lib"], visibility = ["//src:__subpackages__"], ) + +pl_go_test( + name = "dev_dns_updater_test", + srcs = ["dev_dns_updater_test.go"], + embed = [":dev_dns_updater_lib"], + deps = [ + "@io_k8s_api//core/v1:core", + "@io_k8s_apimachinery//pkg/apis/meta/v1:meta", + "@io_k8s_apimachinery//pkg/watch", + ], +) diff --git a/src/utils/dev_dns_updater/dev_dns_updater.go b/src/utils/dev_dns_updater/dev_dns_updater.go index 81146cee793..c7e8f863103 100644 --- a/src/utils/dev_dns_updater/dev_dns_updater.go +++ b/src/utils/dev_dns_updater/dev_dns_updater.go @@ -50,6 +50,7 @@ var dnsEntriesByServiceCfg = map[string][]string{ } var dnsEntriesByService = map[string][]string{} +var lookupIP = net.LookupIP type svcInfo struct { SvcName string @@ -59,7 +60,7 @@ type svcInfo struct { func init() { pflag.String("n", "plc-dev", "The namespace to watch (plc-dev) by default") pflag.String("domain-name", "dev.withpixie.dev", "The domain name to use") - pflag.String("kubeconfig", filepath.Join(homeDir(), ".kube", "config"), "(optional) absolute path to the kubeconfig file") + pflag.String("kubeconfig", defaultKubeconfig(), "(optional) absolute path to the kubeconfig file") } func parseFlags() { @@ -73,7 +74,11 @@ func parseFlags() { // getConfig gets the kubernetes rest config. func getConfig() *rest.Config { // use the current context in kubeconfig - config, err := clientcmd.BuildConfigFromFlags("", viper.GetString("kubeconfig")) + kubeconfig := viper.GetString("kubeconfig") + if kubeconfig == "" { + log.Fatal("Cannot determine homedir, pass in a kubeconfig location explicitly with --kubeconfig") + } + config, err := clientcmd.BuildConfigFromFlags("", kubeconfig) if err != nil { log.WithError(err).Fatal("Could not build kubeconfig") } @@ -91,12 +96,12 @@ func getClientset(config *rest.Config) *kubernetes.Clientset { return clientset } -func homeDir() string { +func defaultKubeconfig() string { hd, err := os.UserHomeDir() - if err != nil && !viper.IsSet("kubeconfig") { - log.Fatal("Cannot determine homedir, pass in a kubeconfig location explicitly with --kubeconfig") + if err != nil { + return "" } - return hd + return filepath.Join(hd, ".kube", "config") } func generateDomainEntries() { @@ -147,11 +152,22 @@ func watchForExternalIP(ch <-chan watch.Event, outCh chan<- svcInfo) error { log.WithField("ing[0].Hostname", ing[0].Hostname). Debug("Using Hostname") - ip, _ := net.LookupIP(ing[0].Hostname) + ips, err := lookupIP(ing[0].Hostname) + if err != nil { + log.WithError(err). + WithField("hostname", ing[0].Hostname). + Warn("Failed to resolve hostname") + continue + } + if len(ips) == 0 { + log.WithField("hostname", ing[0].Hostname). + Warn("Hostname did not resolve to any IPs") + continue + } outCh <- svcInfo{ SvcName: svc.ObjectMeta.Name, - Addr: ip[0].String(), + Addr: ips[0].String(), } } } diff --git a/src/utils/dev_dns_updater/dev_dns_updater_test.go b/src/utils/dev_dns_updater/dev_dns_updater_test.go new file mode 100644 index 00000000000..7812b220104 --- /dev/null +++ b/src/utils/dev_dns_updater/dev_dns_updater_test.go @@ -0,0 +1,114 @@ +/* + * Copyright 2018- The Pixie Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package main + +import ( + "errors" + "net" + "testing" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/watch" +) + +func TestWatchForExternalIPUsesResolvedHostname(t *testing.T) { + restoreDNSState := setTestDNSState() + defer restoreDNSState() + + oldLookupIP := lookupIP + lookupIP = func(host string) ([]net.IP, error) { + if host != "example.com" { + t.Fatalf("unexpected hostname: %s", host) + } + return []net.IP{net.ParseIP("203.0.113.10")}, nil + } + defer func() { lookupIP = oldLookupIP }() + + events := make(chan watch.Event, 1) + out := make(chan svcInfo, 1) + events <- serviceEvent("cloud-proxy-service", "", "example.com") + close(events) + + if err := watchForExternalIP(events, out); err != nil { + t.Fatalf("watchForExternalIP returned error: %v", err) + } + + got := <-out + if got.SvcName != "cloud-proxy-service" { + t.Fatalf("unexpected service name: %s", got.SvcName) + } + if got.Addr != "203.0.113.10" { + t.Fatalf("unexpected address: %s", got.Addr) + } +} + +func TestWatchForExternalIPSkipsUnresolvedHostname(t *testing.T) { + restoreDNSState := setTestDNSState() + defer restoreDNSState() + + oldLookupIP := lookupIP + lookupIP = func(host string) ([]net.IP, error) { + return nil, errors.New("lookup failed") + } + defer func() { lookupIP = oldLookupIP }() + + events := make(chan watch.Event, 1) + out := make(chan svcInfo, 1) + events <- serviceEvent("cloud-proxy-service", "", "example.com") + close(events) + + if err := watchForExternalIP(events, out); err != nil { + t.Fatalf("watchForExternalIP returned error: %v", err) + } + + if len(out) != 0 { + t.Fatalf("expected no service updates, got %d", len(out)) + } +} + +func setTestDNSState() func() { + oldDNSEntries := dnsEntriesByService + dnsEntriesByService = map[string][]string{ + "cloud-proxy-service": {"dev.withpixie.dev"}, + } + return func() { + dnsEntriesByService = oldDNSEntries + } +} + +func serviceEvent(name string, ip string, hostname string) watch.Event { + return watch.Event{ + Object: &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Status: v1.ServiceStatus{ + LoadBalancer: v1.LoadBalancerStatus{ + Ingress: []v1.LoadBalancerIngress{ + { + IP: ip, + Hostname: hostname, + }, + }, + }, + }, + }, + } +}