mirror of
https://github.com/replicatedhq/troubleshoot.git
synced 2026-02-14 10:19:54 +00:00
feat: host collector for DNS (#1617)
* add struct for host dns collector * add miekg/dns * add more logs * nit * new field names * use Hostnames instead of Names * misc update * make schemas * no error when there is no resolv.conf * query all searches * add summary.json file * merge summary into result file * query AAAA and CNAME as well * update schema for hostnames to be required
This commit is contained in:
@@ -1278,6 +1278,19 @@ spec:
|
||||
required:
|
||||
- path
|
||||
type: object
|
||||
dns:
|
||||
properties:
|
||||
collectorName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
hostnames:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
required:
|
||||
- hostnames
|
||||
type: object
|
||||
filesystemPerformance:
|
||||
description: |-
|
||||
FilesystemPerformance benchmarks sequential write latency on a single file.
|
||||
|
||||
@@ -1278,6 +1278,19 @@ spec:
|
||||
required:
|
||||
- path
|
||||
type: object
|
||||
dns:
|
||||
properties:
|
||||
collectorName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
hostnames:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
required:
|
||||
- hostnames
|
||||
type: object
|
||||
filesystemPerformance:
|
||||
description: |-
|
||||
FilesystemPerformance benchmarks sequential write latency on a single file.
|
||||
|
||||
@@ -19850,6 +19850,19 @@ spec:
|
||||
required:
|
||||
- path
|
||||
type: object
|
||||
dns:
|
||||
properties:
|
||||
collectorName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
hostnames:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
required:
|
||||
- hostnames
|
||||
type: object
|
||||
filesystemPerformance:
|
||||
description: |-
|
||||
FilesystemPerformance benchmarks sequential write latency on a single file.
|
||||
|
||||
2
go.mod
2
go.mod
@@ -27,6 +27,7 @@ require (
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/mholt/archiver/v3 v3.5.1
|
||||
github.com/microsoft/go-mssqldb v1.7.2
|
||||
github.com/miekg/dns v1.1.57
|
||||
github.com/opencontainers/image-spec v1.1.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/replicatedhq/termui/v3 v3.1.1-0.20200811145416-f40076d26851
|
||||
@@ -124,6 +125,7 @@ require (
|
||||
go.opentelemetry.io/otel/metric v1.30.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.30.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/tools v0.22.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
|
||||
@@ -218,6 +218,11 @@ type HostJournald struct {
|
||||
Timeout string `json:"timeout,omitempty" yaml:"timeout,omitempty"`
|
||||
}
|
||||
|
||||
type HostDNS struct {
|
||||
HostCollectorMeta `json:",inline" yaml:",inline"`
|
||||
Hostnames []string `json:"hostnames" yaml:"hostnames"`
|
||||
}
|
||||
|
||||
type HostCollect struct {
|
||||
CPU *CPU `json:"cpu,omitempty" yaml:"cpu,omitempty"`
|
||||
Memory *Memory `json:"memory,omitempty" yaml:"memory,omitempty"`
|
||||
@@ -245,6 +250,7 @@ type HostCollect struct {
|
||||
HostKernelConfigs *HostKernelConfigs `json:"kernelConfigs,omitempty" yaml:"kernelConfigs,omitempty"`
|
||||
HostJournald *HostJournald `json:"journald,omitempty" yaml:"journald,omitempty"`
|
||||
HostCGroups *HostCGroups `json:"cgroups,omitempty" yaml:"cgroups,omitempty"`
|
||||
HostDNS *HostDNS `json:"dns,omitempty" yaml:"dns,omitempty"`
|
||||
}
|
||||
|
||||
// GetName gets the name of the collector
|
||||
|
||||
@@ -2129,6 +2129,11 @@ func (in *HostCollect) DeepCopyInto(out *HostCollect) {
|
||||
*out = new(HostCGroups)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.HostDNS != nil {
|
||||
in, out := &in.HostDNS, &out.HostDNS
|
||||
*out = new(HostDNS)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostCollect.
|
||||
@@ -2288,6 +2293,27 @@ func (in *HostCopy) DeepCopy() *HostCopy {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *HostDNS) DeepCopyInto(out *HostDNS) {
|
||||
*out = *in
|
||||
in.HostCollectorMeta.DeepCopyInto(&out.HostCollectorMeta)
|
||||
if in.Hostnames != nil {
|
||||
in, out := &in.Hostnames, &out.Hostnames
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostDNS.
|
||||
func (in *HostDNS) DeepCopy() *HostDNS {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(HostDNS)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *HostHTTP) DeepCopyInto(out *HostHTTP) {
|
||||
*out = *in
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
|
||||
const (
|
||||
dnsUtilsImage = "registry.k8s.io/e2e-test-images/agnhost:2.39"
|
||||
nonResolvableDomain = "non-existent-domain"
|
||||
nonResolvableDomain = "*"
|
||||
)
|
||||
|
||||
type CollectDNS struct {
|
||||
@@ -166,7 +166,7 @@ func troubleshootDNSFromPod(client kubernetes.Interface, ctx context.Context, no
|
||||
echo "=== dig kubernetes ==="
|
||||
dig +search +short kubernetes
|
||||
echo "=== dig non-existent-domain ==="
|
||||
dig +short %s
|
||||
dig +search +short %s
|
||||
exit 0
|
||||
`, nonResolvableDomain)}
|
||||
|
||||
|
||||
@@ -67,6 +67,8 @@ func GetHostCollector(collector *troubleshootv1beta2.HostCollect, bundlePath str
|
||||
return &CollectHostJournald{collector.HostJournald, bundlePath}, true
|
||||
case collector.HostCGroups != nil:
|
||||
return &CollectHostCGroups{collector.HostCGroups, bundlePath}, true
|
||||
case collector.HostDNS != nil:
|
||||
return &CollectHostDNS{collector.HostDNS, bundlePath}, true
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
|
||||
205
pkg/collect/host_dns.go
Normal file
205
pkg/collect/host_dns.go
Normal file
@@ -0,0 +1,205 @@
|
||||
package collect
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/replicatedhq/troubleshoot/internal/util"
|
||||
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
type CollectHostDNS struct {
|
||||
hostCollector *troubleshootv1beta2.HostDNS
|
||||
BundlePath string
|
||||
}
|
||||
|
||||
type DNSResult struct {
|
||||
Query DNSQuery `json:"query"`
|
||||
ResolvedFromSearch string `json:"resolvedFromSearch"`
|
||||
}
|
||||
|
||||
type DNSQuery map[string][]DNSEntry
|
||||
|
||||
type DNSEntry struct {
|
||||
Server string `json:"server"`
|
||||
Search string `json:"search"`
|
||||
Name string `json:"name"`
|
||||
Answer string `json:"answer"`
|
||||
Record string `json:"record"`
|
||||
}
|
||||
|
||||
const (
|
||||
HostDNSPath = "host-collectors/dns/"
|
||||
resolvConf = "/etc/resolv.conf"
|
||||
)
|
||||
|
||||
func (c *CollectHostDNS) Title() string {
|
||||
return hostCollectorTitleOrDefault(c.hostCollector.HostCollectorMeta, "dns")
|
||||
}
|
||||
|
||||
func (c *CollectHostDNS) IsExcluded() (bool, error) {
|
||||
return isExcluded(c.hostCollector.Exclude)
|
||||
}
|
||||
|
||||
func (c *CollectHostDNS) Collect(progressChan chan<- interface{}) (map[string][]byte, error) {
|
||||
|
||||
names := c.hostCollector.Hostnames
|
||||
if len(names) == 0 {
|
||||
return nil, errors.New("hostnames is required")
|
||||
}
|
||||
|
||||
// first, get DNS config from /etc/resolv.conf
|
||||
dnsConfig, err := getDNSConfig()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to read DNS resolve config")
|
||||
}
|
||||
|
||||
// query DNS for each name
|
||||
dnsEntries := make(map[string][]DNSEntry)
|
||||
dnsResult := DNSResult{Query: dnsEntries}
|
||||
allResolvedSearches := []string{}
|
||||
|
||||
for _, name := range names {
|
||||
entries, resolvedSearches, err := resolveName(name, dnsConfig)
|
||||
if err != nil {
|
||||
klog.V(2).Infof("Failed to resolve name %s: %v", name, err)
|
||||
}
|
||||
dnsEntries[name] = entries
|
||||
allResolvedSearches = append(allResolvedSearches, resolvedSearches...)
|
||||
}
|
||||
|
||||
// deduplicate resolved searches
|
||||
dnsResult.ResolvedFromSearch = strings.Join(util.Dedup(allResolvedSearches), ", ")
|
||||
|
||||
// convert dnsResult to a JSON string
|
||||
dnsResultJSON, err := json.MarshalIndent(dnsResult, "", " ")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to marshal DNS query result to JSON")
|
||||
}
|
||||
|
||||
output := NewResult()
|
||||
outputFile := c.getOutputFilePath("result.json")
|
||||
output.SaveResult(c.BundlePath, outputFile, bytes.NewBuffer(dnsResultJSON))
|
||||
|
||||
// write /etc/resolv.conf to a file
|
||||
resolvConfData, err := getResolvConf()
|
||||
if err != nil {
|
||||
klog.V(2).Infof("failed to read DNS resolve config: %v", err)
|
||||
} else {
|
||||
outputFile = c.getOutputFilePath("resolv.conf")
|
||||
output.SaveResult(c.BundlePath, outputFile, bytes.NewBuffer(resolvConfData))
|
||||
}
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func (c *CollectHostDNS) getOutputFilePath(name string) string {
|
||||
// normalize title to be used as a directory name, replace spaces with underscores
|
||||
title := strings.ReplaceAll(c.Title(), " ", "_")
|
||||
return filepath.Join(HostDNSPath, title, name)
|
||||
}
|
||||
|
||||
func getDNSConfig() (*dns.ClientConfig, error) {
|
||||
file, err := os.Open(resolvConf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
config, err := dns.ClientConfigFromFile(file.Name())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse resolv.conf: %v", err)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func resolveName(name string, config *dns.ClientConfig) ([]DNSEntry, []string, error) {
|
||||
|
||||
results := []DNSEntry{}
|
||||
resolvedSearches := []string{}
|
||||
|
||||
// get a name list based on the config
|
||||
queryList := config.NameList(name)
|
||||
klog.V(2).Infof("DNS query list: %v", queryList)
|
||||
|
||||
// for each name in the list, query all the servers
|
||||
// we will query all search domains for each name
|
||||
for _, query := range queryList {
|
||||
for _, server := range config.Servers {
|
||||
klog.V(2).Infof("Querying DNS server %s for name %s", server, query)
|
||||
|
||||
entry := queryDNS(name, query, server+":"+config.Port)
|
||||
results = append(results, entry)
|
||||
|
||||
if entry.Search != "" {
|
||||
resolvedSearches = append(resolvedSearches, entry.Search)
|
||||
}
|
||||
}
|
||||
}
|
||||
return results, resolvedSearches, nil
|
||||
}
|
||||
|
||||
func getResolvConf() ([]byte, error) {
|
||||
data, err := os.ReadFile(resolvConf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func queryDNS(name, query, server string) DNSEntry {
|
||||
recordTypes := []uint16{dns.TypeA, dns.TypeAAAA, dns.TypeCNAME}
|
||||
entry := DNSEntry{Name: query, Server: server, Answer: ""}
|
||||
|
||||
for _, rec := range recordTypes {
|
||||
m := &dns.Msg{}
|
||||
m.SetQuestion(dns.Fqdn(query), rec)
|
||||
in, err := dns.Exchange(m, server)
|
||||
|
||||
if err != nil {
|
||||
klog.Errorf("failed to query DNS server %s for name %s: %v", server, query, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(in.Answer) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
entry.Answer = in.Answer[0].String()
|
||||
|
||||
// remember the search domain that resolved the query
|
||||
// e.g. foo.test.com -> test.com
|
||||
entry.Search = strings.Replace(query, name, "", 1)
|
||||
|
||||
// populate record detail
|
||||
switch rec {
|
||||
case dns.TypeA:
|
||||
record, ok := in.Answer[0].(*dns.A)
|
||||
if ok {
|
||||
entry.Record = record.A.String()
|
||||
}
|
||||
case dns.TypeAAAA:
|
||||
record, ok := in.Answer[0].(*dns.AAAA)
|
||||
if ok {
|
||||
entry.Record = record.AAAA.String()
|
||||
}
|
||||
case dns.TypeCNAME:
|
||||
record, ok := in.Answer[0].(*dns.CNAME)
|
||||
if ok {
|
||||
entry.Record = record.Target
|
||||
}
|
||||
}
|
||||
|
||||
// break on the first successful query
|
||||
break
|
||||
}
|
||||
return entry
|
||||
}
|
||||
@@ -18986,6 +18986,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"dns": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"hostnames"
|
||||
],
|
||||
"properties": {
|
||||
"collectorName": {
|
||||
"type": "string"
|
||||
},
|
||||
"exclude": {
|
||||
"oneOf": [{"type": "string"},{"type": "boolean"}]
|
||||
},
|
||||
"hostnames": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"filesystemPerformance": {
|
||||
"description": "FilesystemPerformance benchmarks sequential write latency on a single file.\nThe optional background IOPS feature attempts to mimic real-world conditions by running read and\nwrite workloads prior to and during benchmark execution.",
|
||||
"type": "object",
|
||||
|
||||
Reference in New Issue
Block a user