├── 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 |

domain-harvester

2 | 3 | # domain-harvester 4 | 5 | [![Release](https://img.shields.io/github/release/shurshun/domain-harvester.svg)](https://github.com/shurshun/domain-harvester/releases/latest) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/shurshun/domain-harvester)](https://goreportcard.com/report/github.com/shurshun/domain-harvester) 7 | [![Powered By: GoReleaser](https://img.shields.io/badge/powered%20by-goreleaser-blue.svg)](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 | --------------------------------------------------------------------------------