├── config.yml
├── .golangci.yml
├── images
└── logo.png
├── Dockerfile
├── .dockerignore
├── .github
├── dependabot.yml
├── release_drafter.yml
└── workflows
│ ├── release_drafter.yml
│ └── goreleaser.yml
├── pkg
├── whois
│ ├── types
│ │ └── types.go
│ ├── whois.go
│ ├── cache
│ │ └── cache.go
│ └── providers
│ │ └── local
│ │ └── local.go
└── k8s
│ └── k8s.go
├── internal
├── harvester
│ ├── helpers
│ │ └── helpers.go
│ ├── types
│ │ └── types.go
│ ├── harvester.go
│ └── modules
│ │ ├── config
│ │ └── config.go
│ │ └── cluster
│ │ └── cluster.go
├── cache
│ ├── internal.go
│ └── cache.go
└── metrics
│ ├── metrics.go
│ └── exporter.go
├── cmd
└── domain-harvester
│ └── main.go
├── .goreleaser.yaml
├── go.mod
├── .helm
└── values.yaml
├── README.md
├── .gitignore
└── go.sum
/config.yml:
--------------------------------------------------------------------------------
1 | projects:
2 | google:
3 | - google.com
4 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | run:
2 | timeout: 3m
3 |
4 | linters:
5 | disable:
6 | - errcheck
--------------------------------------------------------------------------------
/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shurshun/domain-harvester/HEAD/images/logo.png
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM alpine:latest
2 |
3 | COPY domain-harvester /
4 |
5 | ENTRYPOINT ["/domain-harvester"]
6 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | .github
2 | .helm
3 | .git
4 | dist
5 | .gitignore
6 | .dockerignore
7 | README.*
8 | .goreleaser.yml
9 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: gomod
4 | directory: /
5 | schedule:
6 | interval: weekly
7 |
--------------------------------------------------------------------------------
/pkg/whois/types/types.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | type WhoisHarverster interface {
8 | GetDomainData(domain string) (*WhoisData, error)
9 | GetExternalRequestsCnt() uint64
10 | }
11 |
12 | type WhoisData struct {
13 | Domain string
14 | ExpiryDays float64
15 | LastUpdated time.Time
16 | Error string
17 | }
18 |
--------------------------------------------------------------------------------
/pkg/whois/whois.go:
--------------------------------------------------------------------------------
1 | package whois
2 |
3 | import (
4 | "github.com/shurshun/domain-harvester/pkg/whois/cache"
5 | "github.com/shurshun/domain-harvester/pkg/whois/providers/local"
6 | "github.com/shurshun/domain-harvester/pkg/whois/types"
7 | )
8 |
9 | func Init() (types.WhoisHarverster, error) {
10 | wp := &local.WhoisProvider{}
11 |
12 | wp.Init()
13 |
14 | return cache.Init(wp)
15 | }
16 |
--------------------------------------------------------------------------------
/internal/harvester/helpers/helpers.go:
--------------------------------------------------------------------------------
1 | package helpers
2 |
3 | import (
4 | "golang.org/x/net/publicsuffix"
5 | "golang.org/x/net/idna"
6 | )
7 |
8 | func EffectiveTLDPlusOne(domain string) string {
9 | tLDPlusOne, err := publicsuffix.EffectiveTLDPlusOne(domain)
10 | if err != nil {
11 | return domain
12 | }
13 |
14 | return tLDPlusOne
15 | }
16 |
17 | func ToUnicode(name string) string {
18 | p := idna.New()
19 | domain, _ := p.ToUnicode(name)
20 |
21 | return domain
22 | }
--------------------------------------------------------------------------------
/internal/cache/internal.go:
--------------------------------------------------------------------------------
1 | package cache
2 |
3 | import (
4 | "sync"
5 |
6 | "github.com/shurshun/domain-harvester/internal/harvester/types"
7 | )
8 |
9 | type internalCache struct {
10 | mx sync.RWMutex
11 | cache []*types.Domain
12 | }
13 |
14 | func (c *internalCache) Get() []*types.Domain {
15 | c.mx.RLock()
16 | defer c.mx.RUnlock()
17 |
18 | return c.cache
19 | }
20 |
21 | func (c *internalCache) Update(domains []*types.Domain) {
22 | c.mx.Lock()
23 | defer c.mx.Unlock()
24 |
25 | c.cache = domains
26 | }
27 |
--------------------------------------------------------------------------------
/internal/harvester/types/types.go:
--------------------------------------------------------------------------------
1 | package types
2 |
3 | import (
4 | whois_types "github.com/shurshun/domain-harvester/pkg/whois/types"
5 | )
6 |
7 | type Domain struct {
8 | Name string
9 | DisplayName string
10 | Raw string
11 | Source string
12 | Ingress string
13 | NS string
14 | WhoisData *whois_types.WhoisData
15 | }
16 |
17 | type Harvester interface {
18 | // GetDomains() []Domain
19 | }
20 |
21 | type DomainCache interface {
22 | GetDomains() []*Domain
23 | Update(source string, domains []*Domain)
24 | GetExternalRequestsCnt() uint64
25 | }
26 |
--------------------------------------------------------------------------------
/.github/release_drafter.yml:
--------------------------------------------------------------------------------
1 | name-template: 'v$RESOLVED_VERSION'
2 | tag-template: 'v$RESOLVED_VERSION'
3 | template: |
4 | ## Changes
5 | $CHANGES
6 | ## Contributors
7 | $CONTRIBUTORS
8 |
9 | categories:
10 | - title: '⚙️Features'
11 | labels:
12 | - 'enhancement'
13 |
14 | - title: '🔨Bug Fixes'
15 | labels:
16 | - 'bug'
17 |
18 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
19 |
20 | version-resolver:
21 | major:
22 | labels:
23 | - 'major'
24 | minor:
25 | labels:
26 | - 'minor'
27 | patch:
28 | labels:
29 | - 'patch'
30 | default: minor
31 |
--------------------------------------------------------------------------------
/.github/workflows/release_drafter.yml:
--------------------------------------------------------------------------------
1 | name: Release Drafter
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | workflow_dispatch:
8 | inputs:
9 | version:
10 | description: 'Release version'
11 | required: false
12 | branch:
13 | description: 'Target branch'
14 | required: false
15 | default: 'master'
16 |
17 | permissions:
18 | contents: read
19 |
20 | jobs:
21 | update_release_draft:
22 | runs-on: ubuntu-latest
23 | permissions:
24 | contents: write
25 | pull-requests: write
26 | steps:
27 | - uses: release-drafter/release-drafter@v6
28 | with:
29 | config-name: release_drafter.yml
30 | disable-autolabeler: true
31 | version: ${{ github.event.inputs.version }}
32 | commitish: ${{ github.event.inputs.branch }}
33 | env:
34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
35 |
--------------------------------------------------------------------------------
/cmd/domain-harvester/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/shurshun/domain-harvester/internal/harvester"
7 |
8 | log "github.com/sirupsen/logrus"
9 | "github.com/urfave/cli"
10 | )
11 |
12 | var (
13 | Version = "0.1.0"
14 | cliApp = cli.NewApp()
15 | )
16 |
17 | func init() {
18 | cliApp.Version = Version
19 | cliApp.Name = "domain-harvester"
20 | cliApp.Usage = "Collect domains from all ingress resources in the cluster"
21 |
22 | cliApp.Flags = []cli.Flag{
23 | cli.StringFlag{
24 | Name: "kubeconfig",
25 | Usage: "Path to kubernetes config [optional]",
26 | EnvVar: "KUBECONFIG",
27 | },
28 | cli.StringFlag{
29 | Name: "config, c",
30 | Value: "config.yml",
31 | Usage: "Path to config with domains [yaml]",
32 | EnvVar: "CONFIG",
33 | },
34 | cli.StringFlag{
35 | Name: "log-level",
36 | Value: "debug",
37 | Usage: "info/error/debug",
38 | EnvVar: "LOG_LEVEL",
39 | },
40 | cli.StringFlag{
41 | Name: "metrics-addr",
42 | Value: ":8080",
43 | Usage: "Metrics address",
44 | EnvVar: "METRICS_ADDR",
45 | },
46 | }
47 | }
48 |
49 | func main() {
50 | cliApp.Action = harvester.Init
51 | err := cliApp.Run(os.Args)
52 | if err != nil {
53 | log.Fatal(err)
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/internal/harvester/harvester.go:
--------------------------------------------------------------------------------
1 | package harvester
2 |
3 | import (
4 | "github.com/shurshun/domain-harvester/internal/cache"
5 | cluster_harvester "github.com/shurshun/domain-harvester/internal/harvester/modules/cluster"
6 | config_harvester "github.com/shurshun/domain-harvester/internal/harvester/modules/config"
7 | "github.com/shurshun/domain-harvester/internal/metrics"
8 | "github.com/shurshun/domain-harvester/pkg/whois"
9 | log "github.com/sirupsen/logrus"
10 | "github.com/urfave/cli"
11 | )
12 |
13 | func Init(c *cli.Context) error {
14 | setLogLevel(c.String("log-level"))
15 |
16 | whoisProvider, err := whois.Init()
17 | if err != nil {
18 | return err
19 | }
20 |
21 | domainCache, err := cache.Init(whoisProvider)
22 | if err != nil {
23 | return err
24 | }
25 |
26 | _, err = cluster_harvester.Init(c, domainCache)
27 | if err != nil {
28 | return err
29 | }
30 |
31 | _, err = config_harvester.Init(c, domainCache)
32 | if err != nil {
33 | log.Errorf("Can't load config file: %s", err.Error())
34 | }
35 |
36 | return metrics.Init(c, domainCache)
37 | }
38 |
39 | func setLogLevel(logLevel string) {
40 | ll, err := log.ParseLevel(logLevel)
41 |
42 | if err != nil {
43 | log.SetLevel(log.WarnLevel)
44 | } else {
45 | log.SetLevel(ll)
46 | }
47 |
48 | log.SetFormatter(&log.TextFormatter{DisableTimestamp: true})
49 | }
50 |
--------------------------------------------------------------------------------
/.github/workflows/goreleaser.yml:
--------------------------------------------------------------------------------
1 | name: goreleaser
2 |
3 | on:
4 | push:
5 | paths-ignore:
6 | - '.helm/**'
7 | - 'config.yml'
8 | - 'README.md'
9 | pull_request:
10 | paths-ignore:
11 | - '.helm/**'
12 | - 'config.yml'
13 | - 'README.md'
14 |
15 | permissions:
16 | contents: write
17 | packages: write
18 |
19 | jobs:
20 | lint:
21 | runs-on: ubuntu-latest
22 | steps:
23 | - uses: actions/checkout@v4
24 | - uses: golangci/golangci-lint-action@v6
25 | # with:
26 | # only-new-issues: true
27 |
28 | release:
29 | if: startsWith(github.ref, 'refs/tags/v')
30 | needs:
31 | - lint
32 | runs-on: ubuntu-latest
33 | steps:
34 | - name: Checkout
35 | uses: actions/checkout@v4
36 | with:
37 | fetch-depth: 0
38 |
39 | - name: Set up Go
40 | uses: actions/setup-go@v5
41 |
42 | - name: Login to GitHub Container Registry
43 | uses: docker/login-action@v3
44 | with:
45 | registry: ghcr.io
46 | username: ${{ github.actor }}
47 | password: ${{ secrets.GITHUB_TOKEN }}
48 |
49 | - name: Run GoReleaser
50 | uses: goreleaser/goreleaser-action@v6
51 | with:
52 | version: '~> v2'
53 | args: release --clean
54 | env:
55 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
56 |
--------------------------------------------------------------------------------
/pkg/whois/cache/cache.go:
--------------------------------------------------------------------------------
1 | package cache
2 |
3 | import (
4 | "sync"
5 | "time"
6 |
7 | "github.com/shurshun/domain-harvester/pkg/whois/types"
8 | )
9 |
10 | type WhoisCache struct {
11 | cache sync.Map
12 | whoisProvider types.WhoisHarverster
13 | }
14 |
15 | func Init(whoisProvider types.WhoisHarverster) (types.WhoisHarverster, error) {
16 | wc := &WhoisCache{whoisProvider: whoisProvider}
17 |
18 | go wc.runInvalidator()
19 |
20 | return wc, nil
21 | }
22 |
23 | func shouldUpdate(wd *types.WhoisData) bool {
24 | return wd.ExpiryDays < 30 || time.Since(wd.LastUpdated).Minutes() > 60
25 | }
26 |
27 | func (wc *WhoisCache) GetExternalRequestsCnt() uint64 {
28 | return wc.whoisProvider.GetExternalRequestsCnt()
29 | }
30 |
31 | func (wc *WhoisCache) runInvalidator() {
32 | for {
33 | wc.cache.Range(func(k, v interface{}) bool {
34 | if shouldUpdate(v.(*types.WhoisData)) {
35 | wc.update(k.(string))
36 | }
37 |
38 | return true
39 | })
40 |
41 | time.Sleep(time.Minute * 1)
42 | }
43 | }
44 |
45 | func (wc *WhoisCache) update(domain string) (*types.WhoisData, error) {
46 | wd, err := wc.whoisProvider.GetDomainData(domain)
47 |
48 | wc.cache.Store(domain, wd)
49 |
50 | return wd, err
51 | }
52 |
53 | func (wc *WhoisCache) GetDomainData(domain string) (*types.WhoisData, error) {
54 | wd, ok := wc.cache.Load(domain)
55 |
56 | if ok {
57 | return wd.(*types.WhoisData), nil
58 | }
59 |
60 | return wc.update(domain)
61 | }
62 |
--------------------------------------------------------------------------------
/pkg/k8s/k8s.go:
--------------------------------------------------------------------------------
1 | package k8s
2 |
3 | import (
4 | "errors"
5 | "github.com/urfave/cli"
6 | "k8s.io/client-go/kubernetes"
7 | "k8s.io/client-go/rest"
8 | "k8s.io/client-go/tools/clientcmd"
9 | "os"
10 | "path/filepath"
11 | )
12 |
13 | func Init(c *cli.Context) (*kubernetes.Clientset, error) {
14 | if areWeInsideACluster() {
15 | return getInClusterClient()
16 | }
17 |
18 | return getOutClusterClient(c.String("kubeconfig"))
19 | }
20 |
21 | func areWeInsideACluster() bool {
22 | fi, err := os.Stat("/var/run/secrets/kubernetes.io/serviceaccount/token")
23 | return os.Getenv("KUBERNETES_SERVICE_HOST") != "" &&
24 | os.Getenv("KUBERNETES_SERVICE_PORT") != "" &&
25 | err == nil && !fi.IsDir()
26 | }
27 |
28 | func getInClusterClient() (*kubernetes.Clientset, error) {
29 | config, err := rest.InClusterConfig()
30 | if err != nil {
31 | return nil, err
32 | }
33 |
34 | return kubernetes.NewForConfig(config)
35 | }
36 |
37 | func getOutClusterClient(k8sConfigPath string) (*kubernetes.Clientset, error) {
38 | var configPath string
39 |
40 | if k8sConfigPath != "" {
41 | configPath = k8sConfigPath
42 | } else if home := homeDir(); home != "" {
43 | configPath = filepath.Join(home, ".kube", "config")
44 | } else {
45 | return nil, errors.New("k8s config can't be found")
46 | }
47 |
48 | config, err := clientcmd.BuildConfigFromFlags("", configPath)
49 | if err != nil {
50 | return nil, err
51 | }
52 |
53 | return kubernetes.NewForConfig(config)
54 | }
55 |
56 | func homeDir() string {
57 | if h := os.Getenv("HOME"); h != "" {
58 | return h
59 | }
60 | return os.Getenv("USERPROFILE") // windows
61 | }
62 |
--------------------------------------------------------------------------------
/internal/metrics/metrics.go:
--------------------------------------------------------------------------------
1 | package metrics
2 |
3 | import (
4 | "net/http"
5 | "net/http/pprof"
6 |
7 | "github.com/prometheus/client_golang/prometheus"
8 | "github.com/prometheus/client_golang/prometheus/collectors"
9 | "github.com/prometheus/client_golang/prometheus/promhttp"
10 | "github.com/shurshun/domain-harvester/internal/harvester/types"
11 | log "github.com/sirupsen/logrus"
12 | "github.com/urfave/cli"
13 | )
14 |
15 | func Init(c *cli.Context, domainCache types.DomainCache) error {
16 | r := http.NewServeMux()
17 |
18 | r.HandleFunc("/_liveness", livenessHandler)
19 | r.HandleFunc("/_readiness", readinessHandler)
20 |
21 | r.HandleFunc("/debug/pprof/", pprof.Index)
22 | r.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
23 | r.HandleFunc("/debug/pprof/profile", pprof.Profile)
24 | r.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
25 | r.HandleFunc("/debug/pprof/trace", pprof.Trace)
26 |
27 | r.Handle("/metrics", promhttp.Handler())
28 |
29 | r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
30 | w.Write([]byte(`
31 |
Domain Harvester
32 |
33 | Domain Harvester
34 | Metrics
35 |
36 | `))
37 | })
38 |
39 | prometheus.MustRegister(collectors.NewBuildInfoCollector())
40 | prometheus.MustRegister(NewDomainExporter(domainCache))
41 |
42 | log.Infof("ready to handle requests at %s", c.String("metrics-addr"))
43 |
44 | return http.ListenAndServe(c.String("metrics-addr"), r)
45 | }
46 |
47 | func readinessHandler(w http.ResponseWriter, r *http.Request) {
48 | w.Write([]byte("OK"))
49 | }
50 |
51 | func livenessHandler(w http.ResponseWriter, r *http.Request) {
52 | w.Write([]byte("OK"))
53 | }
54 |
--------------------------------------------------------------------------------
/internal/harvester/modules/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/shurshun/domain-harvester/internal/harvester/helpers"
7 | "github.com/shurshun/domain-harvester/internal/harvester/types"
8 | "github.com/urfave/cli"
9 | "gopkg.in/yaml.v2"
10 | // log "github.com/sirupsen/logrus"
11 | )
12 |
13 | const source = "config"
14 |
15 | type Config struct {
16 | Projects map[string][]string `yaml:"projects"`
17 | }
18 |
19 | type ConfigHarverster struct {
20 | // domains []types.Domain
21 | config Config
22 | domainCache types.DomainCache
23 | }
24 |
25 | func Init(c *cli.Context, domainCache types.DomainCache) (types.Harvester, error) {
26 | harvester := &ConfigHarverster{domainCache: domainCache}
27 |
28 | configPath := c.String("config")
29 |
30 | _, err := os.Stat(configPath)
31 | if err != nil {
32 | return harvester, err
33 | }
34 |
35 | f, err := os.Open(configPath)
36 | if err != nil {
37 | return harvester, err
38 | }
39 | defer f.Close()
40 |
41 | harvester.config = Config{}
42 |
43 | decoder := yaml.NewDecoder(f)
44 | err = decoder.Decode(&harvester.config)
45 | if err != nil {
46 | return harvester, err
47 | }
48 |
49 | harvester.domainCache.Update(source, harvester.getDomains())
50 |
51 | return harvester, nil
52 | }
53 |
54 | func (ch *ConfigHarverster) getDomains() []*types.Domain {
55 | var result []*types.Domain
56 |
57 | for project, domains := range ch.config.Projects {
58 | for _, domain := range domains {
59 | result = append(result, &types.Domain{
60 | Name: helpers.EffectiveTLDPlusOne(domain),
61 | DisplayName: helpers.ToUnicode(helpers.EffectiveTLDPlusOne(domain)),
62 | Raw: domain,
63 | Source: source,
64 | Ingress: project,
65 | NS: project,
66 | })
67 | }
68 | }
69 |
70 | return result
71 | }
72 |
--------------------------------------------------------------------------------
/pkg/whois/providers/local/local.go:
--------------------------------------------------------------------------------
1 | package local
2 |
3 | import (
4 | "fmt"
5 | "math"
6 | "regexp"
7 | "strings"
8 |
9 | whois "github.com/shift/whois"
10 | "github.com/shurshun/domain-harvester/pkg/whois/types"
11 |
12 | // "sync"
13 | "time"
14 | )
15 |
16 | var formats = []string{
17 | "2006-01-02",
18 | "2006-01-02T15:04:05Z",
19 | "02-Jan-2006",
20 | "2006.01.02",
21 | "Mon Jan 2 15:04:05 MST 2006",
22 | "02/01/2006",
23 | "2006-01-02 15:04:05 MST",
24 | "2006/01/02",
25 | "Mon Jan 2006 15:04:05",
26 | }
27 |
28 | type WhoisProvider struct {
29 | regexpTime *regexp.Regexp
30 | // mutex sync.RWMutex
31 | externalRequests uint64
32 | }
33 |
34 | func withError(wd *types.WhoisData, err error) (*types.WhoisData, error) {
35 | wd.Error = err.Error()
36 |
37 | return wd, err
38 | }
39 |
40 | func (wp *WhoisProvider) Init() {
41 | wp.regexpTime, _ = regexp.Compile(`(Registry Expiry Date|paid-till|Expiration Date|Expiry.*): (.*)`)
42 | wp.externalRequests = 0
43 | }
44 |
45 | func (wp *WhoisProvider) GetExternalRequestsCnt() uint64 {
46 | return wp.externalRequests
47 | }
48 |
49 | func (wp *WhoisProvider) GetDomainData(domain string) (*types.WhoisData, error) {
50 | wd := &types.WhoisData{Domain: domain, ExpiryDays: 0, LastUpdated: time.Now(), Error: ""}
51 |
52 | wp.externalRequests++
53 |
54 | req, err := whois.NewRequest(domain)
55 | if err != nil {
56 | return withError(wd, err)
57 | }
58 |
59 | var res *whois.Response
60 |
61 | res, err = whois.DefaultClient.Fetch(req)
62 | if err != nil {
63 | return withError(wd, err)
64 | }
65 | result := wp.regexpTime.FindStringSubmatch(res.String())
66 |
67 | if len(result) < 2 {
68 | return withError(wd, fmt.Errorf("Don't know how to parse data: %s", res.String()))
69 | }
70 |
71 | rawDate := strings.TrimSpace(result[2])
72 |
73 | for _, format := range formats {
74 | if date, err := time.Parse(format, rawDate); err == nil {
75 | wd.ExpiryDays = math.Floor(time.Until(date).Hours() / 24)
76 |
77 | return wd, nil
78 | }
79 | }
80 |
81 | return withError(wd, fmt.Errorf("Unable to parse date: %s", rawDate))
82 | }
83 |
--------------------------------------------------------------------------------
/internal/metrics/exporter.go:
--------------------------------------------------------------------------------
1 | package metrics
2 |
3 | import (
4 | "github.com/shurshun/domain-harvester/internal/harvester/types"
5 |
6 | "github.com/prometheus/client_golang/prometheus"
7 | )
8 |
9 | const (
10 | namespace = "domain"
11 | subsystem = ""
12 | )
13 |
14 | // Exporter collects metrics from a bitcoin server.
15 | type Exporter struct {
16 | domainCache types.DomainCache
17 |
18 | expiryDays *prometheus.Desc
19 | lastUpdated *prometheus.Desc
20 | updateError *prometheus.Desc
21 | whoisRequests *prometheus.Desc
22 | }
23 |
24 | func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
25 | prometheus.DescribeByCollect(e, ch)
26 | }
27 |
28 | func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
29 | domains := e.domainCache.GetDomains()
30 |
31 | for _, d := range domains {
32 | ch <- prometheus.MustNewConstMetric(e.expiryDays, prometheus.GaugeValue, d.WhoisData.ExpiryDays, d.DisplayName, d.Raw, d.Source, d.Ingress, d.NS)
33 | }
34 |
35 | for _, d := range domains {
36 | ch <- prometheus.MustNewConstMetric(e.lastUpdated, prometheus.GaugeValue, float64(d.WhoisData.LastUpdated.Unix()), d.DisplayName, d.Raw, d.Source, d.Ingress, d.NS)
37 | }
38 |
39 | for _, d := range domains {
40 | var err float64
41 |
42 | if d.WhoisData.Error != "" {
43 | err = 1
44 | } else {
45 | err = 0
46 | }
47 |
48 | ch <- prometheus.MustNewConstMetric(e.updateError, prometheus.GaugeValue, err, d.DisplayName, d.Raw, d.Source, d.Ingress, d.NS)
49 | }
50 |
51 | ch <- prometheus.MustNewConstMetric(e.whoisRequests, prometheus.CounterValue, float64(e.domainCache.GetExternalRequestsCnt()))
52 | }
53 |
54 | func NewDomainExporter(domainCache types.DomainCache) *Exporter {
55 | e := &Exporter{
56 | domainCache: domainCache,
57 | expiryDays: prometheus.NewDesc(
58 | prometheus.BuildFQName(namespace, subsystem, "expiry_days"),
59 | "time in days until the domain expires",
60 | []string{"domain", "fqdn", "source", "ingress", "ingress_namespace"},
61 | nil,
62 | ),
63 | lastUpdated: prometheus.NewDesc(
64 | prometheus.BuildFQName(namespace, subsystem, "last_updated"),
65 | "last update of the domain",
66 | []string{"domain", "fqdn", "source", "ingress", "ingress_namespace"},
67 | nil,
68 | ),
69 | updateError: prometheus.NewDesc(
70 | prometheus.BuildFQName(namespace, subsystem, "update_error"),
71 | "error on domain update",
72 | []string{"domain", "fqdn", "source", "ingress", "ingress_namespace"},
73 | nil,
74 | ),
75 | whoisRequests: prometheus.NewDesc(
76 | prometheus.BuildFQName(namespace, subsystem, "whois_requests"),
77 | "requests to the whois servers",
78 | nil,
79 | nil,
80 | ),
81 | }
82 |
83 | return e
84 | }
85 |
--------------------------------------------------------------------------------
/.goreleaser.yaml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | before:
4 | hooks:
5 | # You may remove this if you don't use go modules.
6 | - go mod tidy
7 | # you may remove this if you don't need go generate
8 | - go generate ./...
9 |
10 | builds:
11 | - env:
12 | - CGO_ENABLED=0
13 | goos:
14 | - linux
15 | - darwin
16 | goarch:
17 | - amd64
18 | - arm64
19 | ldflags:
20 | - -s -w -X "main.Version={{ .Version }}"
21 | main: "./cmd/domain-harvester/main.go"
22 |
23 | source:
24 | enabled: false
25 |
26 | archives:
27 | - format: tar.gz
28 | # this name template makes the OS and Arch compatible with the results of `uname`.
29 | name_template: >-
30 | {{ .ProjectName }}_
31 | {{- title .Os }}_
32 | {{- if eq .Arch "amd64" }}x86_64
33 | {{- else if eq .Arch "386" }}i386
34 | {{- else }}{{ .Arch }}{{ end }}
35 | {{- if .Arm }}v{{ .Arm }}{{ end }}
36 |
37 | changelog:
38 | sort: asc
39 | filters:
40 | exclude:
41 | - "^docs:"
42 | - "^test:"
43 |
44 | docker_manifests:
45 | - name_template: ghcr.io/shurshun/{{ .ProjectName }}:{{ .Version }}
46 | image_templates:
47 | - ghcr.io/shurshun/{{ .ProjectName }}:{{ .Version }}-amd64
48 | - ghcr.io/shurshun/{{ .ProjectName }}:{{ .Version }}-arm64
49 |
50 | dockers:
51 | - image_templates:
52 | - ghcr.io/shurshun/{{ .ProjectName }}:{{ .Version }}-amd64
53 | use: buildx
54 | goos: linux
55 | goarch: amd64
56 | build_flag_templates:
57 | - --pull
58 | - --platform=linux/amd64
59 | - --label=org.opencontainers.image.title={{ .ProjectName }}
60 | - --label=org.opencontainers.image.description={{ .ProjectName }}
61 | - --label=org.opencontainers.image.url=https://github.com/shurshun/{{ .ProjectName }}
62 | - --label=org.opencontainers.image.source=https://github.com/shurshun/{{ .ProjectName }}
63 | - --label=org.opencontainers.image.version={{ .Version }}
64 | - --label=org.opencontainers.image.created={{ time "2006-01-02T15:04:05Z07:00" }}
65 | - --label=org.opencontainers.image.revision={{ .FullCommit }}
66 | - image_templates:
67 | - ghcr.io/shurshun/{{ .ProjectName }}:{{ .Version }}-arm64
68 | use: buildx
69 | goos: linux
70 | goarch: arm64
71 | build_flag_templates:
72 | - --pull
73 | - --platform=linux/arm64
74 | - --label=org.opencontainers.image.title={{ .ProjectName }}
75 | - --label=org.opencontainers.image.description={{ .ProjectName }}
76 | - --label=org.opencontainers.image.url=https://github.com/shurshun/{{ .ProjectName }}
77 | - --label=org.opencontainers.image.source=https://github.com/shurshun/{{ .ProjectName }}
78 | - --label=org.opencontainers.image.version={{ .Version }}
79 | - --label=org.opencontainers.image.created={{ time "2006-01-02T15:04:05Z07:00" }}
80 | - --label=org.opencontainers.image.revision={{ .FullCommit }}
81 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/shurshun/domain-harvester
2 |
3 | go 1.24.0
4 |
5 | require (
6 | github.com/bep/debounce v1.2.1
7 | github.com/prometheus/client_golang v1.22.0
8 | github.com/shift/whois v0.0.0-20160722035721-a6942ea71fce
9 | github.com/sirupsen/logrus v1.9.3
10 | github.com/urfave/cli v1.22.17
11 | golang.org/x/net v0.42.0
12 | gopkg.in/yaml.v2 v2.4.0
13 | k8s.io/api v0.33.3
14 | k8s.io/apimachinery v0.33.3
15 | k8s.io/client-go v0.33.3
16 | )
17 |
18 | require (
19 | github.com/PuerkitoBio/goquery v1.9.2 // indirect
20 | github.com/andybalholm/cascadia v1.3.2 // indirect
21 | github.com/beorn7/perks v1.0.1 // indirect
22 | github.com/cespare/xxhash/v2 v2.3.0 // indirect
23 | github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
24 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
25 | github.com/domainr/whoistest v0.0.0-20240826103427-f811a0715270 // indirect
26 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect
27 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect
28 | github.com/go-logr/logr v1.4.2 // indirect
29 | github.com/go-openapi/jsonpointer v0.21.0 // indirect
30 | github.com/go-openapi/jsonreference v0.20.2 // indirect
31 | github.com/go-openapi/swag v0.23.0 // indirect
32 | github.com/gogo/protobuf v1.3.2 // indirect
33 | github.com/google/gnostic-models v0.6.9 // indirect
34 | github.com/google/go-cmp v0.7.0 // indirect
35 | github.com/google/uuid v1.6.0 // indirect
36 | github.com/josharian/intern v1.0.0 // indirect
37 | github.com/json-iterator/go v1.1.12 // indirect
38 | github.com/mailru/easyjson v0.7.7 // indirect
39 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
40 | github.com/modern-go/reflect2 v1.0.2 // indirect
41 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
42 | github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 // indirect
43 | github.com/pkg/errors v0.9.1 // indirect
44 | github.com/prometheus/client_model v0.6.1 // indirect
45 | github.com/prometheus/common v0.62.0 // indirect
46 | github.com/prometheus/procfs v0.15.1 // indirect
47 | github.com/russross/blackfriday/v2 v2.1.0 // indirect
48 | github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
49 | github.com/spf13/pflag v1.0.5 // indirect
50 | github.com/x448/float16 v0.8.4 // indirect
51 | github.com/zonedb/zonedb v1.0.4818 // indirect
52 | golang.org/x/oauth2 v0.27.0 // indirect
53 | golang.org/x/sys v0.34.0 // indirect
54 | golang.org/x/term v0.33.0 // indirect
55 | golang.org/x/text v0.27.0 // indirect
56 | golang.org/x/time v0.9.0 // indirect
57 | google.golang.org/protobuf v1.36.5 // indirect
58 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
59 | gopkg.in/inf.v0 v0.9.1 // indirect
60 | gopkg.in/yaml.v3 v3.0.1 // indirect
61 | k8s.io/klog/v2 v2.130.1 // indirect
62 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
63 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
64 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
65 | sigs.k8s.io/randfill v1.0.0 // indirect
66 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect
67 | sigs.k8s.io/yaml v1.4.0 // indirect
68 | )
69 |
--------------------------------------------------------------------------------
/.helm/values.yaml:
--------------------------------------------------------------------------------
1 | image:
2 | repository: ghcr.io/shurshun/domain-harvester
3 | tag: "1.4.0"
4 |
5 | nameOverride: domain-harvester
6 | fullnameOverride: domain-harvester
7 |
8 | ports:
9 | metrics:
10 | port: 8080
11 | labels:
12 | prometheus.io/scrape: "true"
13 |
14 | configs:
15 | - name: config.yml
16 | path: /config.yml
17 | data: |-
18 | projects:
19 | google:
20 | - google.com
21 |
22 | env:
23 | raw:
24 | LOG_LEVEL: debug
25 |
26 | livenessProbe:
27 | httpGet:
28 | path: /_liveness
29 | port: metrics
30 | scheme: HTTP
31 | failureThreshold: 3
32 | initialDelaySeconds: 10
33 | periodSeconds: 10
34 | successThreshold: 1
35 | timeoutSeconds: 1
36 |
37 | readinessProbe:
38 | httpGet:
39 | path: /_readiness
40 | port: metrics
41 | scheme: HTTP
42 | failureThreshold: 3
43 | initialDelaySeconds: 10
44 | periodSeconds: 10
45 | successThreshold: 1
46 | timeoutSeconds: 1
47 |
48 | monitoring:
49 | prometheus:
50 | # https://github.com/helm/charts/blob/master/stable/prometheus-operator/values.yaml#L1642
51 | labels:
52 | prom_rules: cluster
53 | release: mon
54 | groups:
55 | domains:
56 | DomainUpdateError:
57 | annotations:
58 | description: "{{$labels.source}}/{{$labels.ns}}/{{$labels.ingress}}/{{$labels.name}} can't update whois information! \n"
59 | summary: DomainUpdateError
60 | expr: domain_update_error > 0
61 | for: 1m
62 | labels:
63 | severity: critical
64 | DomainLastUpdate:
65 | annotations:
66 | description: "{{$labels.source}}/{{$labels.ns}}/{{$labels.ingress}}/{{$labels.name}} updated more than 86400 seconds ago \n"
67 | summary: |
68 | DomainLastUpdate
69 | expr: time() - domain_last_updated > 86400
70 | for: 2m
71 | labels:
72 | severity: warning
73 | DomainExpiryWarn:
74 | annotations:
75 | description: "{{$labels.source}}/{{$labels.ns}}/{{$labels.ingress}}/{{$labels.name}} expires in {{ $value }} days \n"
76 | summary: |
77 | DomainExpiryWarn
78 | expr: (domain_expiry_days < 30) and (domain_expiry_days > 14) and (domain_update_error == 0)
79 | for: 2m
80 | labels:
81 | severity: warning
82 | DomainExpiryCritical:
83 | annotations:
84 | description: "{{$labels.source}}/{{$labels.ns}}/{{$labels.ingress}}/{{$labels.name}} expires in {{ $value }} days \n"
85 | summary: |
86 | DomainExpiryCritical
87 | expr: (domain_expiry_days < 15) and (domain_update_error == 0)
88 | for: 2m
89 | labels:
90 | severity: critical
91 |
92 | rbac:
93 | enabled: true
94 | namespaced: false
95 | roleRef: |
96 | apiGroup: rbac.authorization.k8s.io
97 | kind: ClusterRole
98 | name: {{ template "go-app.fullname" . }}
99 | rules:
100 | - apiGroups: ["extensions"]
101 | resources: ["ingresses"]
102 | verbs: ["get", "watch", "list"]
103 |
--------------------------------------------------------------------------------
/internal/cache/cache.go:
--------------------------------------------------------------------------------
1 | package cache
2 |
3 | import (
4 | "sync"
5 | "sync/atomic"
6 | "time"
7 |
8 | "github.com/bep/debounce"
9 | "github.com/shurshun/domain-harvester/internal/harvester/types"
10 | whois_types "github.com/shurshun/domain-harvester/pkg/whois/types"
11 | log "github.com/sirupsen/logrus"
12 | )
13 |
14 | type DomainCache struct {
15 | rawCache sync.Map
16 | intCache *internalCache
17 | whoisProvider whois_types.WhoisHarverster
18 | debounceCounter uint64
19 | debounced func(f func())
20 | }
21 |
22 | func Init(whoisProvider whois_types.WhoisHarverster) (types.DomainCache, error) {
23 | dc := &DomainCache{
24 | intCache: &internalCache{},
25 | whoisProvider: whoisProvider,
26 | debounced: debounce.New(1 * time.Second),
27 | }
28 |
29 | go dc.runCacheInvalidator()
30 |
31 | return dc, nil
32 | }
33 |
34 | func (dc *DomainCache) runCacheInvalidator() {
35 | for {
36 | dc.rebuildDomainCache()
37 |
38 | time.Sleep(time.Minute * 1)
39 | }
40 | }
41 |
42 | func (dc *DomainCache) GetDomains() []*types.Domain {
43 | return dc.intCache.Get()
44 | }
45 |
46 | func (dc *DomainCache) GetExternalRequestsCnt() uint64 {
47 | return dc.whoisProvider.GetExternalRequestsCnt()
48 | }
49 |
50 | func (dc *DomainCache) Update(source string, domains []*types.Domain) {
51 | dc.rawCache.Store(source, domains)
52 |
53 | f := func() {
54 | atomic.AddUint64(&dc.debounceCounter, 1)
55 | dc.rebuildDomainCache()
56 | }
57 |
58 | dc.debounced(f)
59 | }
60 |
61 | func (dc *DomainCache) getUniqDomains() (result []*types.Domain) {
62 | domains := make(map[string]bool)
63 |
64 | dc.rawCache.Range(func(k, v interface{}) bool {
65 | for _, domain := range v.([]*types.Domain) {
66 | if _, ok := domains[domain.Name]; !ok {
67 | domains[domain.Name] = true
68 | result = append(result, domain)
69 | }
70 | }
71 |
72 | return true
73 | })
74 |
75 | return result
76 | }
77 |
78 | func (dc *DomainCache) rebuildDomainCache() {
79 | uniqDomains := dc.getUniqDomains()
80 |
81 | if len(uniqDomains) == 0 {
82 | return
83 | }
84 |
85 | log.Debugf("Start rebuilding cache for %d domains...", len(uniqDomains))
86 |
87 | var newCache []*types.Domain
88 |
89 | var wg sync.WaitGroup
90 |
91 | startTime := time.Now()
92 |
93 | queue := make(chan *types.Domain, len(uniqDomains))
94 |
95 | for _, domain := range uniqDomains {
96 | wg.Add(1)
97 | go func(wg *sync.WaitGroup, whoisProvider whois_types.WhoisHarverster, domain *types.Domain, queue chan *types.Domain) {
98 | defer wg.Done()
99 |
100 | wd, err := whoisProvider.GetDomainData(domain.Name)
101 | if err != nil {
102 | log.Debugf("Error on update %s domain: %s", wd.Domain, wd.Error)
103 | }
104 |
105 | domain.WhoisData = wd
106 |
107 | queue <- domain
108 | }(&wg, dc.whoisProvider, domain, queue)
109 | }
110 |
111 | go func() {
112 | defer close(queue)
113 | wg.Wait()
114 | }()
115 |
116 | for domain := range queue {
117 | newCache = append(newCache, domain)
118 | }
119 |
120 | dc.intCache.Update(newCache)
121 |
122 | elapsedTime := time.Since(startTime)
123 |
124 | log.Debugf("Domain cache has been updated in %s", elapsedTime)
125 | }
126 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # domain-harvester
4 |
5 | [](https://github.com/shurshun/domain-harvester/releases/latest)
6 | [](https://goreportcard.com/report/github.com/shurshun/domain-harvester)
7 | [](https://github.com/goreleaser)
8 |
9 | App collects domains from all Ingress resources in a Kubernetes cluster and provides its expiry information.
10 |
11 | ## Domain sources
12 |
13 | * Kubernetes Ingress Resource
14 | * Config file
15 |
16 | ## Metrics example
17 | App provides 3 metrics per domain and 1 metric with total number of the requests to the whois servers.
18 |
19 | ```
20 | # HELP domain_expiry_days time in days until the domain expires
21 | # TYPE domain_expiry_days gauge
22 | domain_expiry_days{ingress="google",domain="google.com",ingress_namespace="google",fqdn="google.com",source="config"} 3014
23 | domain_expiry_days{ingress="example",domain="example.com",ingress_namespace="default",fqdn="test.example.com",source="cluster"} 341
24 | # HELP domain_last_updated last update of the domain
25 | # TYPE domain_last_updated gauge
26 | domain_last_updated{ingress="google",domain="google.com",ingress_namespace="google",fqdn="google.com",source="config"} 1.592078203e+09
27 | domain_last_updated{ingress="example",domain="example.com"",ingress_namespace="default",fqdn="test.example.com",source="cluster"} 1.592078203e+09
28 | # HELP domain_update_error error on domain update
29 | # TYPE domain_update_error gauge
30 | domain_update_error{ingress="google",domain="google.com",ingress_namespace="google",fqdn="google.com",source="config"} 0
31 | domain_update_error{ingress="example",domain="example.com"",ingress_namespace="default",fqdn="test.example.com",source="cluster"} 0
32 | # HELP domain_whois_requests requests to the whois servers
33 | # TYPE domain_whois_requests counter
34 | domain_whois_requests 2
35 | ```
36 |
37 | ## Installation
38 |
39 | * **via binary**
40 |
41 | Just download and run binary for your platform https://github.com/shurshun/domain-harvester/releases
42 |
43 | * **via docker**
44 |
45 | ```
46 | docker run --rm -it -v ~/.kube/config:/root/.kube/config -p 8080:8080 ghcr.io/shurshun/domain-harvester:1.4.0
47 | ```
48 |
49 | * **via helm**
50 |
51 | Application could be installed using my own Helm chart [go-app](https://github.com/shurshun/go-app-chart)
52 |
53 | ```
54 | helm repo add shurshun https://shurshun.github.com/helm-charts
55 | helm repo update
56 | helm upgrade --install domain-harverster shurshun/go-app -f https://raw.githubusercontent.com/shurshun/domain-harvester/master/.helm/values.yaml
57 | ```
58 |
59 | ## Configuration options
60 |
61 | ```
62 | --kubeconfig value Path to kubernetes config [optional] [$KUBECONFIG]
63 | --config value, -c value Path to config with domains [yaml] (default: "config.yml") [$CONFIG]
64 | --log-level value info/error/debug (default: "debug") [$LOG_LEVEL]
65 | --metrics-addr value Metrics address (default: ":8080") [$METRICS_ADDR]
66 | --help, -h show help
67 | --version, -v print the version
68 | ```
69 |
70 | ## Example of the optional config file
71 |
72 | ```
73 | projects:
74 | google:
75 | - google.com
76 |
77 | ```
78 |
79 | ## Support
80 |
81 | For any additional information, please, contact me via telegram [@shursh](https://t.me/shursh)
82 |
83 |
--------------------------------------------------------------------------------
/internal/harvester/modules/cluster/cluster.go:
--------------------------------------------------------------------------------
1 | package cluster
2 |
3 | import (
4 | "github.com/shurshun/domain-harvester/internal/harvester/helpers"
5 | "github.com/shurshun/domain-harvester/internal/harvester/types"
6 | "github.com/shurshun/domain-harvester/pkg/k8s"
7 |
8 | log "github.com/sirupsen/logrus"
9 | "github.com/urfave/cli"
10 | v1 "k8s.io/api/core/v1"
11 | networkingv1 "k8s.io/api/networking/v1"
12 | "k8s.io/apimachinery/pkg/fields"
13 | "k8s.io/apimachinery/pkg/util/wait"
14 | "k8s.io/client-go/tools/cache"
15 | )
16 |
17 | const source = "cluster"
18 |
19 | type ClusterHarverster struct {
20 | ingressCache cache.Store
21 | domainCache types.DomainCache
22 | }
23 |
24 | func Init(c *cli.Context, domainCache types.DomainCache) (types.Harvester, error) {
25 | harvester := &ClusterHarverster{domainCache: domainCache}
26 |
27 | k8sClient, err := k8s.Init(c)
28 | if err != nil {
29 | return harvester, err
30 | }
31 |
32 | watchlist := cache.NewListWatchFromClient(k8sClient.NetworkingV1().RESTClient(), "ingresses", v1.NamespaceAll, fields.Everything())
33 |
34 | iStore, iController := cache.NewInformerWithOptions(
35 | cache.InformerOptions{
36 | ListerWatcher: watchlist,
37 | ObjectType: &networkingv1.Ingress{},
38 | ResyncPeriod: 0,
39 | Handler: cache.ResourceEventHandlerFuncs{
40 | AddFunc: harvester.ingressCreated,
41 | UpdateFunc: harvester.ingressUpdated,
42 | DeleteFunc: harvester.ingressDeleted,
43 | },
44 | },
45 | )
46 |
47 | go iController.Run(wait.NeverStop)
48 |
49 | harvester.ingressCache = iStore
50 |
51 | return harvester, nil
52 | }
53 |
54 | func (ch *ClusterHarverster) ingressCreated(obj interface{}) {
55 | ingress := obj.(*networkingv1.Ingress)
56 |
57 | log.WithFields(log.Fields{
58 | "name": ingress.ObjectMeta.Name,
59 | "action": "create",
60 | }).Debug("Found new ingress")
61 |
62 | ch.domainCache.Update(source, ch.getDomains())
63 | }
64 |
65 | func (ch *ClusterHarverster) ingressUpdated(oldObj, newObj interface{}) {
66 | ingress := newObj.(*networkingv1.Ingress)
67 |
68 | log.WithFields(log.Fields{
69 | "name": ingress.ObjectMeta.Name,
70 | "action": "update",
71 | }).Debug("Ingress has been updated")
72 |
73 | ch.domainCache.Update(source, ch.getDomains())
74 | }
75 |
76 | func (ch *ClusterHarverster) ingressDeleted(obj interface{}) {
77 | ingress := obj.(*networkingv1.Ingress)
78 |
79 | log.WithFields(log.Fields{
80 | "name": ingress.ObjectMeta.Name,
81 | "action": "delete",
82 | }).Debug("Ingress was deleted")
83 |
84 | ch.domainCache.Update(source, ch.getDomains())
85 | }
86 |
87 | func (ch *ClusterHarverster) getDomains() []*types.Domain {
88 | var result []*types.Domain
89 |
90 | for _, obj := range ch.ingressCache.List() {
91 | ingress := obj.(*networkingv1.Ingress)
92 |
93 | for _, rule := range ingress.Spec.Rules {
94 | if rule.Host == "" {
95 | log.WithFields(log.Fields{
96 | "name": ingress.ObjectMeta.Name,
97 | "action": "skip",
98 | }).Debug("Ingress rule has no host")
99 |
100 | continue
101 | }
102 |
103 | result = append(result, &types.Domain{
104 | Name: helpers.EffectiveTLDPlusOne(rule.Host),
105 | DisplayName: helpers.ToUnicode(helpers.EffectiveTLDPlusOne(rule.Host)),
106 | Raw: rule.Host,
107 | Source: source,
108 | Ingress: ingress.ObjectMeta.Name,
109 | NS: ingress.ObjectMeta.Namespace,
110 | })
111 | }
112 | }
113 |
114 | return result
115 | }
116 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | pprof.*
3 | .idea
4 | images
5 |
6 | # Helm
7 | *.dec
8 |
9 | # Created by https://www.gitignore.io/api/go,macos,linux,phpstorm
10 |
11 | ### Go ###
12 | # Binaries for programs and plugins
13 | *.exe
14 | *.exe~
15 | *.dll
16 | *.so
17 | *.dylib
18 |
19 | # Test binary, build with `go test -c`
20 | *.test
21 |
22 | # Output of the go coverage tool, specifically when used with LiteIDE
23 | *.out
24 |
25 | ### Go Patch ###
26 | /vendor/
27 | /Godeps/
28 |
29 | ### Linux ###
30 | *~
31 |
32 | # temporary files which can be created if a process still has a handle open of a deleted file
33 | .fuse_hidden*
34 |
35 | # KDE directory preferences
36 | .directory
37 |
38 | # Linux trash folder which might appear on any partition or disk
39 | .Trash-*
40 |
41 | # .nfs files are created when an open file is removed but is still being accessed
42 | .nfs*
43 |
44 | ### macOS ###
45 | # General
46 | .DS_Store
47 | .AppleDouble
48 | .LSOverride
49 |
50 | # Icon must end with two \r
51 | Icon
52 |
53 | # Thumbnails
54 | ._*
55 |
56 | # Files that might appear in the root of a volume
57 | .DocumentRevisions-V100
58 | .fseventsd
59 | .Spotlight-V100
60 | .TemporaryItems
61 | .Trashes
62 | .VolumeIcon.icns
63 | .com.apple.timemachine.donotpresent
64 |
65 | # Directories potentially created on remote AFP share
66 | .AppleDB
67 | .AppleDesktop
68 | Network Trash Folder
69 | Temporary Items
70 | .apdisk
71 |
72 | ### PhpStorm ###
73 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
74 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
75 |
76 | # User-specific stuff
77 | .idea/**/workspace.xml
78 | .idea/**/tasks.xml
79 | .idea/**/usage.statistics.xml
80 | .idea/**/dictionaries
81 | .idea/**/shelf
82 |
83 | # Sensitive or high-churn files
84 | .idea/**/dataSources/
85 | .idea/**/dataSources.ids
86 | .idea/**/dataSources.local.xml
87 | .idea/**/sqlDataSources.xml
88 | .idea/**/dynamic.xml
89 | .idea/**/uiDesigner.xml
90 | .idea/**/dbnavigator.xml
91 |
92 | # Gradle
93 | .idea/**/gradle.xml
94 | .idea/**/libraries
95 |
96 | # Gradle and Maven with auto-import
97 | # When using Gradle or Maven with auto-import, you should exclude module files,
98 | # since they will be recreated, and may cause churn. Uncomment if using
99 | # auto-import.
100 | # .idea/modules.xml
101 | # .idea/*.iml
102 | # .idea/modules
103 |
104 | # CMake
105 | cmake-build-*/
106 |
107 | # Mongo Explorer plugin
108 | .idea/**/mongoSettings.xml
109 |
110 | # File-based project format
111 | *.iws
112 |
113 | # IntelliJ
114 | out/
115 |
116 | # mpeltonen/sbt-idea plugin
117 | .idea_modules/
118 |
119 | # JIRA plugin
120 | atlassian-ide-plugin.xml
121 |
122 | # Cursive Clojure plugin
123 | .idea/replstate.xml
124 |
125 | # Crashlytics plugin (for Android Studio and IntelliJ)
126 | com_crashlytics_export_strings.xml
127 | crashlytics.properties
128 | crashlytics-build.properties
129 | fabric.properties
130 |
131 | # Editor-based Rest Client
132 | .idea/httpRequests
133 |
134 | ### PhpStorm Patch ###
135 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
136 |
137 | # *.iml
138 | # modules.xml
139 | # .idea/misc.xml
140 | # *.ipr
141 |
142 | # Sonarlint plugin
143 | .idea/sonarlint
144 |
145 | ### Node ###
146 | # Logs
147 | logs
148 | *.log
149 | npm-debug.log*
150 | yarn-debug.log*
151 | yarn-error.log*
152 |
153 | # Runtime data
154 | pids
155 | *.pid
156 | *.seed
157 | *.pid.lock
158 |
159 | # Directory for instrumented libs generated by jscoverage/JSCover
160 | lib-cov
161 |
162 | # Coverage directory used by tools like istanbul
163 | coverage
164 |
165 | # nyc test coverage
166 | .nyc_output
167 |
168 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
169 | .grunt
170 |
171 | # Bower dependency directory (https://bower.io/)
172 | bower_components
173 |
174 | # node-waf configuration
175 | .lock-wscript
176 |
177 | # Compiled binary addons (https://nodejs.org/api/addons.html)
178 | build/Release
179 |
180 | # Dependency directories
181 | node_modules/
182 | jspm_packages/
183 |
184 | # TypeScript v1 declaration files
185 | typings/
186 |
187 | # Optional npm cache directory
188 | .npm
189 |
190 | # Optional eslint cache
191 | .eslintcache
192 |
193 | # Optional REPL history
194 | .node_repl_history
195 |
196 | # Output of 'npm pack'
197 | *.tgz
198 |
199 | # Yarn Integrity file
200 | .yarn-integrity
201 |
202 | # dotenv environment variables file
203 | .env
204 |
205 | # parcel-bundler cache (https://parceljs.org/)
206 | .cache
207 |
208 | # next.js build output
209 | .next
210 |
211 | # nuxt.js build output
212 | .nuxt
213 |
214 | # vuepress build output
215 | .vuepress/dist
216 |
217 | # Serverless directories
218 | .serverless
219 |
220 | # End of https://www.gitignore.io/api/node
221 |
222 | dist/
223 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
2 | github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE=
3 | github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
4 | github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
5 | github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
6 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
7 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
8 | github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
9 | github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
10 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
11 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
12 | github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
13 | github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
14 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
15 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
16 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
17 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
18 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
19 | github.com/domainr/whoistest v0.0.0-20240826103427-f811a0715270 h1:xvAnD8cKT0cR6DdQivElzH1seiba2DOUm/M+7wfc4OU=
20 | github.com/domainr/whoistest v0.0.0-20240826103427-f811a0715270/go.mod h1:g9QP3pAcrwGQT2IUIPsFktvHLfqzmp6lKedwurO2T6c=
21 | github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
22 | github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
23 | github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
24 | github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
25 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
26 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
27 | github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
28 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
29 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
30 | github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
31 | github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
32 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
33 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
34 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
35 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
36 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
37 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
38 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
39 | github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw=
40 | github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw=
41 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
42 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
43 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
44 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
45 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
46 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
47 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
48 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
49 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
50 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
51 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
52 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
53 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
54 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
55 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
56 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
57 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
58 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
59 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
60 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
61 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
62 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
63 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
64 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
65 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
66 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
67 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
68 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
69 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
70 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
71 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
72 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
73 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
74 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
75 | github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
76 | github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
77 | github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
78 | github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
79 | github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
80 | github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
81 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
82 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
83 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
84 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
85 | github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
86 | github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
87 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
88 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
89 | github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
90 | github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
91 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
92 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
93 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
94 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
95 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
96 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
97 | github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA=
98 | github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
99 | github.com/shift/whois v0.0.0-20160722035721-a6942ea71fce h1:5xLsI+auBWyS76o6ACbsu20euFcmPgXcINHH5JVdpEQ=
100 | github.com/shift/whois v0.0.0-20160722035721-a6942ea71fce/go.mod h1:AQVvAUhT1MVAy3VNDKcmflZITwIDEJAiN0WffRKFkHM=
101 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
102 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
103 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
104 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
105 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
106 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
107 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
108 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
109 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
110 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
111 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
112 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
113 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
114 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
115 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
116 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
117 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
118 | github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ=
119 | github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo=
120 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
121 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
122 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
123 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
124 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
125 | github.com/zonedb/zonedb v1.0.4818 h1:nHo8oukBY6ALBp5p/m0AWXgJDuORPc+hjaIVDQIFiDQ=
126 | github.com/zonedb/zonedb v1.0.4818/go.mod h1:LgMJaQynuMdG/5jkQiqZHBnZ/bXOm372XLJsAd6a23c=
127 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
128 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
129 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
130 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
131 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
132 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
133 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
134 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
135 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
136 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
137 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
138 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
139 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
140 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
141 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
142 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
143 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
144 | golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
145 | golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
146 | golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
147 | golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
148 | golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
149 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
150 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
151 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
152 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
153 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
154 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
155 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
156 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
157 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
158 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
159 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
160 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
161 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
162 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
163 | golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
164 | golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
165 | golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
166 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
167 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
168 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
169 | golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
170 | golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
171 | golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
172 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
173 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
174 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
175 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
176 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
177 | golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
178 | golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
179 | golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
180 | golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
181 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
182 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
183 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
184 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
185 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
186 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
187 | golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
188 | golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
189 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
190 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
191 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
192 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
193 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
194 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
195 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
196 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
197 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
198 | gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
199 | gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
200 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
201 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
202 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
203 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
204 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
205 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
206 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
207 | k8s.io/api v0.33.3 h1:SRd5t//hhkI1buzxb288fy2xvjubstenEKL9K51KBI8=
208 | k8s.io/api v0.33.3/go.mod h1:01Y/iLUjNBM3TAvypct7DIj0M0NIZc+PzAHCIo0CYGE=
209 | k8s.io/apimachinery v0.33.3 h1:4ZSrmNa0c/ZpZJhAgRdcsFcZOw1PQU1bALVQ0B3I5LA=
210 | k8s.io/apimachinery v0.33.3/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
211 | k8s.io/client-go v0.33.3 h1:M5AfDnKfYmVJif92ngN532gFqakcGi6RvaOF16efrpA=
212 | k8s.io/client-go v0.33.3/go.mod h1:luqKBQggEf3shbxHY4uVENAxrDISLOarxpTKMiUuujg=
213 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
214 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
215 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
216 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8=
217 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
218 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
219 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
220 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
221 | sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
222 | sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
223 | sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
224 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc=
225 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
226 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
227 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
228 |
--------------------------------------------------------------------------------