Skip to content

Commit

Permalink
feat: host collector for DNS (#1617)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
nvanthao authored Sep 19, 2024
1 parent d73082a commit 8823f7d
Show file tree
Hide file tree
Showing 10 changed files with 302 additions and 2 deletions.
13 changes: 13 additions & 0 deletions config/crds/troubleshoot.sh_hostcollectors.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
13 changes: 13 additions & 0 deletions config/crds/troubleshoot.sh_hostpreflights.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
13 changes: 13 additions & 0 deletions config/crds/troubleshoot.sh_supportbundles.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions pkg/apis/troubleshoot/v1beta2/hostcollector_shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions pkg/collect/dns.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import (

const (
dnsUtilsImage = "registry.k8s.io/e2e-test-images/agnhost:2.39"
nonResolvableDomain = "non-existent-domain"
nonResolvableDomain = "*"
)

type CollectDNS struct {
Expand Down Expand Up @@ -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)}

Expand Down
2 changes: 2 additions & 0 deletions pkg/collect/host_collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
205 changes: 205 additions & 0 deletions pkg/collect/host_dns.go
Original file line number Diff line number Diff line change
@@ -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
}
20 changes: 20 additions & 0 deletions schemas/supportbundle-troubleshoot-v1beta2.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit 8823f7d

Please sign in to comment.