├── .gitignore ├── LICENSE ├── README.md ├── build.sh ├── examples ├── kubegroup-example │ ├── groupcache.go │ └── main.go └── kubegroup-example3 │ ├── groupcache.go │ └── main.go ├── go.mod ├── go.sum └── kubegroup ├── kubegroup.go └── metrics.go /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | go.work.sum 23 | 24 | # env file 25 | .env 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 udhos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![license](http://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/udhos/kubegroup/blob/main/LICENSE) 2 | [![Go Report Card](https://goreportcard.com/badge/github.com/udhos/kubegroup)](https://goreportcard.com/report/github.com/udhos/kubegroup) 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/udhos/kubegroup.svg)](https://pkg.go.dev/github.com/udhos/kubegroup) 4 | 5 | # kubegroup 6 | 7 | [kubegroup](https://github.com/udhos/kubegroup) provides peer autodiscovery for pods running [groupcache](https://github.com/mailgun/groupcache) within a kubernetes cluster. 8 | 9 | Peer pods are automatically discovered by continuously watching for other pods with the same label `app=` as in the current pod, in current pod's namespace. 10 | 11 | # Metrics 12 | 13 | ``` 14 | kubegroup_peers: Gauge: Number of peer PODs discovered. 15 | kubegroup_events: Counter: Number of events received. 16 | ``` 17 | 18 | # Usage for groupcache3 19 | 20 | Import these packages. 21 | 22 | ```go 23 | import ( 24 | "github.com/groupcache/groupcache-go/v3" 25 | "github.com/groupcache/groupcache-go/v3/transport" 26 | "github.com/udhos/kube/kubeclient" 27 | "github.com/udhos/kubegroup/kubegroup" 28 | ) 29 | ``` 30 | 31 | Initialize groupcache3 and add kubegroup auto-discovery with `kubegroup.UpdatePeers()`. 32 | 33 | ```go 34 | groupcachePort := ":5000" 35 | 36 | // 1. start groupcache3 daemon 37 | 38 | myIP, errAddr := kubegroup.FindMyAddress() 39 | if errAddr != nil { 40 | log.Fatalf("find my address: %v", errAddr) 41 | } 42 | 43 | myAddr := myIP + groupCachePort 44 | 45 | daemon, errDaemon := groupcache.ListenAndServe(context.TODO(), myAddr, groupcache.Options{}) 46 | if errDaemon != nil { 47 | log.Fatalf("groupcache daemon: %v", errDaemon) 48 | } 49 | 50 | // 2. spawn peering autodiscovery 51 | 52 | const debug = true 53 | 54 | clientsetOpt := kubeclient.Options{DebugLog: debug} 55 | clientset, errClientset := kubeclient.New(clientsetOpt) 56 | if errClientset != nil { 57 | log.Fatalf("kubeclient: %v", errClientset) 58 | } 59 | 60 | options := kubegroup.Options{ 61 | Client: clientset, 62 | Peers: daemon, 63 | LabelSelector: "app=miniapi", 64 | GroupCachePort: groupCachePort, 65 | Debug: debug, 66 | MetricsRegisterer: prometheus.DefaultRegisterer, 67 | MetricsGatherer: prometheus.DefaultGatherer, 68 | } 69 | 70 | discoveryGroup, errDiscovery := kubegroup.UpdatePeers(options) 71 | if errDiscovery != nil { 72 | log.Fatalf("kubegroup: %v", errDiscovery) 73 | } 74 | 75 | // 3. create groupcache groups 76 | 77 | ttl := time.Minute 78 | 79 | getter := groupcache.GetterFunc( 80 | func(_ context.Context, filePath string, dest transport.Sink) error { 81 | 82 | log.Printf("cache miss, loading file: %s (ttl:%v)", 83 | filePath, ttl) 84 | 85 | data, errRead := os.ReadFile(filePath) 86 | if errRead != nil { 87 | return errRead 88 | } 89 | 90 | var expire time.Time // zero value for expire means no expiration 91 | if app.groupCacheExpire != 0 { 92 | expire = time.Now().Add(ttl) 93 | } 94 | 95 | return dest.SetBytes(data, expire) 96 | }, 97 | ) 98 | 99 | cache, errGroup := daemon.NewGroup("files", 1_000_000, getter) 100 | if errGroup != nil { 101 | log.Fatalf("new group: %v", errGroup) 102 | } 103 | 104 | // 4. query cache 105 | 106 | var data []byte 107 | 108 | errGet := cache.Get(context.TODO(), "filename.txt", transport.AllocatingByteSliceSink(&data)) 109 | if errGet != nil { 110 | log.Printf("cache error: %v", errGet) 111 | } else { 112 | log.Printf("cache response: %s", string(data)) 113 | } 114 | 115 | // 5. when you need to stop kubegroup auto-discovery 116 | 117 | discoveryGroup.Close() // release kubegroup resources 118 | ``` 119 | 120 | # Example for groupcache3 121 | 122 | See [./examples/kubegroup-example3](./examples/kubegroup-example3) 123 | 124 | # Usage for groupcache2 125 | 126 | Import the package `github.com/udhos/kubegroup/kubegroup`. 127 | 128 | ```go 129 | import "github.com/udhos/kubegroup/kubegroup" 130 | ``` 131 | 132 | Initialize auto-discovery with `kubegroup.UpdatePeers()`. 133 | 134 | ```go 135 | groupcachePort := ":5000" 136 | 137 | // 1. get my groupcache URL 138 | myURL, errURL = kubegroup.FindMyURL(groupcachePort) 139 | 140 | workspace := groupcache.NewWorkspace() 141 | 142 | // 2. spawn groupcache peering server 143 | pool := groupcache.NewHTTPPoolOptsWithWorkspace(workspace, myURL, &groupcache.HTTPPoolOptions{}) 144 | server := &http.Server{Addr: groupcachePort, Handler: pool} 145 | go func() { 146 | log.Printf("groupcache server: listening on %s", groupcachePort) 147 | err := server.ListenAndServe() 148 | log.Printf("groupcache server: exited: %v", err) 149 | }() 150 | 151 | // 3. spawn peering autodiscovery 152 | 153 | clientset, errClientset := kubeclient.New(kubeclient.Options{}) 154 | if errClientset != nil { 155 | log.Fatalf("kubeclient: %v", errClientset) 156 | } 157 | 158 | options := kubegroup.Options{ 159 | Client: clientset, 160 | Pool: pool, 161 | LabelSelector: "app=my-app", 162 | GroupCachePort: groupCachePort, 163 | MetricsRegisterer: prometheus.DefaultRegisterer, 164 | MetricsGatherer: prometheus.DefaultGatherer, 165 | } 166 | 167 | group, errGroup := kubegroup.UpdatePeers(options) 168 | if errGroup != nil { 169 | log.Fatalf("kubegroup: %v", errGroup) 170 | } 171 | 172 | // 4. create groupcache groups, etc: groupOne := groupcache.NewGroup() 173 | 174 | // 5. before shutdown you might stop kubegroup to release resources 175 | 176 | group.Close() // release kubegroup resources 177 | server.Shutdown(...) // do not forget to shutdown the peering server 178 | ``` 179 | 180 | # Example 181 | 182 | See [./examples/kubegroup-example](./examples/kubegroup-example) 183 | 184 | # POD Permissions 185 | 186 | The application PODs will need permissions to get/list/watch PODs against kubernetes API, as illustrated by the role below. 187 | 188 | ## Role 189 | 190 | ```yaml 191 | apiVersion: rbac.authorization.k8s.io/v1 192 | kind: Role 193 | metadata: 194 | name: role-name 195 | rules: 196 | - apiGroups: 197 | - "" 198 | resources: 199 | - 'pods' 200 | verbs: 201 | - 'get' 202 | - 'list' 203 | - 'watch' 204 | ``` 205 | 206 | Example chart template: https://github.com/udhos/gateboard/blob/main/charts/gateboard/templates/role.yaml 207 | 208 | ## Role Binding 209 | 210 | ```yaml 211 | apiVersion: rbac.authorization.k8s.io/v1 212 | kind: RoleBinding 213 | metadata: 214 | name: rolebinding-name 215 | roleRef: 216 | apiGroup: rbac.authorization.k8s.io 217 | kind: Role 218 | name: role-name 219 | subjects: 220 | - kind: ServiceAccount 221 | name: put-pod-service-account-here 222 | namespace: put-pod-namespace-here 223 | ``` 224 | 225 | Example chart template: https://github.com/udhos/gateboard/blob/main/charts/gateboard/templates/rolebinding.yaml 226 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | go install golang.org/x/vuln/cmd/govulncheck@latest 4 | go install golang.org/x/tools/cmd/deadcode@latest 5 | go install github.com/mgechev/revive@latest 6 | 7 | gofmt -s -w . 8 | 9 | revive ./... 10 | 11 | gocyclo -over 15 . 12 | 13 | go mod tidy 14 | 15 | govulncheck ./... 16 | 17 | deadcode ./examples/* 18 | 19 | go env -w CGO_ENABLED=1 20 | 21 | go test -race ./... 22 | 23 | go env -w CGO_ENABLED=0 24 | 25 | go install ./... 26 | 27 | go env -u CGO_ENABLED -------------------------------------------------------------------------------- /examples/kubegroup-example/groupcache.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "net/http" 7 | "os" 8 | "time" 9 | 10 | "github.com/modernprogram/groupcache/v2" 11 | "github.com/udhos/dogstatsdclient/dogstatsdclient" 12 | "github.com/udhos/kube/kubeclient" 13 | "github.com/udhos/kubegroup/kubegroup" 14 | ) 15 | 16 | func startGroupcache(app *application, dogstatsd, mockDogstatsd bool) { 17 | 18 | // 19 | // create groupcache pool 20 | // 21 | 22 | myURL, errURL := kubegroup.FindMyURL(app.groupCachePort) 23 | if errURL != nil { 24 | log.Fatalf("my URL: %v", errURL) 25 | } 26 | log.Printf("groupcache my URL: %s", myURL) 27 | 28 | workspace := groupcache.NewWorkspace() 29 | 30 | pool := groupcache.NewHTTPPoolOptsWithWorkspace(workspace, myURL, 31 | &groupcache.HTTPPoolOptions{}) 32 | 33 | // 34 | // start groupcache server 35 | // 36 | 37 | app.serverGroupCache = &http.Server{Addr: app.groupCachePort, Handler: pool} 38 | 39 | go func() { 40 | log.Printf("groupcache server: listening on %s", app.groupCachePort) 41 | err := app.serverGroupCache.ListenAndServe() 42 | log.Printf("groupcache server: exited: %v", err) 43 | }() 44 | 45 | // 46 | // start watcher for addresses of peers 47 | // 48 | 49 | const debug = true 50 | 51 | clientsetOpt := kubeclient.Options{DebugLog: debug} 52 | clientset, errClientset := kubeclient.New(clientsetOpt) 53 | if errClientset != nil { 54 | log.Fatalf("startGroupcache: kubeclient: %v", errClientset) 55 | } 56 | 57 | var dogstatsdClient kubegroup.DogstatsdClient 58 | if dogstatsd { 59 | if mockDogstatsd { 60 | dogstatsdClient = &kubegroup.DogstatsdClientMock{} 61 | } else { 62 | c, errClient := dogstatsdclient.New(dogstatsdclient.Options{ 63 | Namespace: "kubegroup", 64 | Debug: debug, 65 | }) 66 | if errClient != nil { 67 | log.Fatalf("dogstatsd client: %v", errClient) 68 | } 69 | dogstatsdClient = c 70 | } 71 | } 72 | 73 | options := kubegroup.Options{ 74 | Client: clientset, 75 | Pool: pool, 76 | LabelSelector: "app=miniapi", 77 | GroupCachePort: app.groupCachePort, 78 | Debug: debug, 79 | DogstatsdClient: dogstatsdClient, 80 | ForceNamespaceDefault: true, 81 | } 82 | 83 | if app.registry != nil { 84 | options.MetricsRegisterer = app.registry 85 | options.MetricsGatherer = app.registry 86 | } 87 | 88 | group, errGroup := kubegroup.UpdatePeers(options) 89 | if errGroup != nil { 90 | log.Fatalf("kubegroup: %v", errGroup) 91 | } 92 | 93 | app.group = group 94 | 95 | // 96 | // create cache 97 | // 98 | 99 | getter := groupcache.GetterFunc( 100 | func(_ /*ctx*/ context.Context, filePath string, dest groupcache.Sink, _ *groupcache.Info) error { 101 | 102 | log.Printf("cache miss, loading file: %s (ttl:%v)", 103 | filePath, app.groupCacheExpire) 104 | 105 | data, errRead := os.ReadFile(filePath) 106 | if errRead != nil { 107 | return errRead 108 | } 109 | 110 | var expire time.Time // zero value for expire means no expiration 111 | if app.groupCacheExpire != 0 { 112 | expire = time.Now().Add(app.groupCacheExpire) 113 | } 114 | 115 | dest.SetBytes(data, expire) 116 | 117 | return nil 118 | }, 119 | ) 120 | 121 | const purgeExpired = true 122 | 123 | groupcacheOptions := groupcache.Options{ 124 | Workspace: workspace, 125 | Name: "files", 126 | PurgeExpired: purgeExpired, 127 | CacheBytesLimit: app.groupCacheSizeBytes, 128 | Getter: getter, 129 | } 130 | 131 | // https://talks.golang.org/2013/oscon-dl.slide#46 132 | // 133 | // 64 MB max per-node memory usage 134 | app.cache = groupcache.NewGroupWithWorkspace(groupcacheOptions) 135 | } 136 | -------------------------------------------------------------------------------- /examples/kubegroup-example/main.go: -------------------------------------------------------------------------------- 1 | // Package main implements the example. 2 | package main 3 | 4 | import ( 5 | "context" 6 | "flag" 7 | "log" 8 | "net/http" 9 | "os" 10 | "os/signal" 11 | "strings" 12 | "syscall" 13 | "time" 14 | 15 | "github.com/modernprogram/groupcache/v2" 16 | "github.com/prometheus/client_golang/prometheus" 17 | "github.com/prometheus/client_golang/prometheus/collectors" 18 | "github.com/prometheus/client_golang/prometheus/promhttp" 19 | "github.com/udhos/kubegroup/kubegroup" 20 | ) 21 | 22 | type application struct { 23 | listenAddr string 24 | groupCachePort string 25 | groupCacheSizeBytes int64 26 | groupCacheExpire time.Duration 27 | 28 | serverMain *http.Server 29 | serverGroupCache *http.Server 30 | cache *groupcache.Group 31 | group *kubegroup.Group 32 | 33 | registry *prometheus.Registry 34 | } 35 | 36 | func main() { 37 | 38 | var dogstatsd bool 39 | var prom bool 40 | var mockDogstatsd bool 41 | flag.BoolVar(&dogstatsd, "dogstatsd", true, "enable dogstatsd") 42 | flag.BoolVar(&prom, "prom", true, "enable prometheus") 43 | flag.BoolVar(&mockDogstatsd, "mockDogstatsd", true, "mock dogstatsd") 44 | flag.Parse() 45 | log.Printf("dogstatds=%t prom=%t mockDogstatsd=%t", 46 | dogstatsd, prom, mockDogstatsd) 47 | 48 | mux := http.NewServeMux() 49 | 50 | app := &application{ 51 | listenAddr: ":8080", 52 | groupCachePort: ":5000", 53 | groupCacheSizeBytes: 1_000_000, // limit cache at 1 MB 54 | groupCacheExpire: 60 * time.Second, // cache TTL at 60s 55 | } 56 | 57 | // 58 | // metrics 59 | // 60 | 61 | if prom { 62 | app.registry = prometheus.NewRegistry() 63 | 64 | app.registry.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{})) 65 | app.registry.MustRegister(collectors.NewGoCollector()) 66 | 67 | mux.Handle("/metrics", app.metricsHandler()) 68 | } 69 | 70 | // 71 | // main server 72 | // 73 | 74 | startGroupcache(app, dogstatsd, mockDogstatsd) 75 | 76 | app.serverMain = &http.Server{Addr: app.listenAddr, Handler: mux} 77 | 78 | mux.HandleFunc("/", func(w http.ResponseWriter, 79 | r *http.Request) { 80 | routeHandler(w, r, app) 81 | }) 82 | 83 | go func() { 84 | // 85 | // start main http server 86 | // 87 | log.Printf("main server: listening on %s", app.listenAddr) 88 | err := app.serverMain.ListenAndServe() 89 | log.Printf("main server: exited: %v", err) 90 | }() 91 | 92 | shutdown(app) 93 | 94 | log.Printf("exiting") 95 | } 96 | 97 | func (app *application) metricsHandler() http.Handler { 98 | registerer := app.registry 99 | gatherer := app.registry 100 | return promhttp.InstrumentMetricHandler( 101 | registerer, promhttp.HandlerFor(gatherer, promhttp.HandlerOpts{}), 102 | ) 103 | } 104 | 105 | func routeHandler(w http.ResponseWriter, r *http.Request, app *application) { 106 | 107 | filePath := r.URL.Path 108 | 109 | filePath = strings.TrimPrefix(filePath, "/") 110 | 111 | var data []byte 112 | errGet := app.cache.Get(r.Context(), filePath, groupcache.AllocatingByteSliceSink(&data), nil) 113 | if errGet != nil { 114 | log.Printf("routeHandler: %s %s: cache error: %v", r.Method, r.URL.Path, errGet) 115 | http.Error(w, errGet.Error(), 500) 116 | return 117 | } 118 | 119 | if _, errWrite := w.Write(data); errWrite != nil { 120 | log.Printf("routeHandler: %s %s: write error: %v", r.Method, r.URL.Path, errWrite) 121 | } 122 | } 123 | 124 | func shutdown(app *application) { 125 | quit := make(chan os.Signal, 1) 126 | signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) 127 | sig := <-quit 128 | 129 | log.Printf("received signal '%v', initiating shutdown", sig) 130 | 131 | log.Printf("stopping kubegroup") 132 | 133 | app.group.Close() // release kubegroup resources 134 | 135 | time.Sleep(time.Second) // give kubegroup time to log debug messages about exiting 136 | 137 | log.Printf("stopping http servers") 138 | 139 | httpShutdown(app.serverMain) 140 | httpShutdown(app.serverGroupCache) 141 | } 142 | 143 | func httpShutdown(server *http.Server) { 144 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 145 | defer cancel() 146 | if err := server.Shutdown(ctx); err != nil { 147 | log.Printf("http server shutdown error: %v", err) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /examples/kubegroup-example3/groupcache.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "os" 7 | "time" 8 | 9 | "github.com/groupcache/groupcache-go/v3" 10 | "github.com/groupcache/groupcache-go/v3/transport" 11 | "github.com/udhos/dogstatsdclient/dogstatsdclient" 12 | "github.com/udhos/kube/kubeclient" 13 | "github.com/udhos/kubegroup/kubegroup" 14 | ) 15 | 16 | func startGroupcache(app *application, dogstatsd, mockDogstatsd bool) { 17 | 18 | // 19 | // create groupcache instance 20 | // 21 | 22 | myIP, errAddr := kubegroup.FindMyAddress() 23 | if errAddr != nil { 24 | log.Fatalf("find my address: %v", errAddr) 25 | } 26 | 27 | myAddr := myIP + app.groupCachePort 28 | 29 | daemon, errDaemon := groupcache.ListenAndServe(context.TODO(), myAddr, groupcache.Options{}) 30 | if errDaemon != nil { 31 | log.Fatalf("groupcache daemon: %v", errDaemon) 32 | } 33 | 34 | // 35 | // start watcher for addresses of peers 36 | // 37 | 38 | const debug = true 39 | 40 | clientsetOpt := kubeclient.Options{DebugLog: debug} 41 | clientset, errClientset := kubeclient.New(clientsetOpt) 42 | if errClientset != nil { 43 | log.Fatalf("startGroupcache: kubeclient: %v", errClientset) 44 | } 45 | 46 | var dogstatsdClient kubegroup.DogstatsdClient 47 | if dogstatsd { 48 | if mockDogstatsd { 49 | dogstatsdClient = &kubegroup.DogstatsdClientMock{} 50 | } else { 51 | c, errClient := dogstatsdclient.New(dogstatsdclient.Options{ 52 | Namespace: "kubegroup", 53 | Debug: debug, 54 | }) 55 | if errClient != nil { 56 | log.Fatalf("dogstatsd client: %v", errClient) 57 | } 58 | dogstatsdClient = c 59 | } 60 | } 61 | 62 | options := kubegroup.Options{ 63 | Client: clientset, 64 | Peers: daemon, 65 | LabelSelector: "app=miniapi", 66 | GroupCachePort: app.groupCachePort, 67 | Debug: debug, 68 | DogstatsdClient: dogstatsdClient, 69 | ForceNamespaceDefault: true, 70 | } 71 | 72 | if app.registry != nil { 73 | options.MetricsRegisterer = app.registry 74 | options.MetricsGatherer = app.registry 75 | } 76 | 77 | group, errGroup := kubegroup.UpdatePeers(options) 78 | if errGroup != nil { 79 | log.Fatalf("kubegroup: %v", errGroup) 80 | } 81 | 82 | app.group = group 83 | 84 | // 85 | // create cache 86 | // 87 | 88 | getter := groupcache.GetterFunc( 89 | func(_ context.Context, filePath string, dest transport.Sink) error { 90 | 91 | log.Printf("cache miss, loading file: %s (ttl:%v)", 92 | filePath, app.groupCacheExpire) 93 | 94 | data, errRead := os.ReadFile(filePath) 95 | if errRead != nil { 96 | return errRead 97 | } 98 | 99 | var expire time.Time // zero value for expire means no expiration 100 | if app.groupCacheExpire != 0 { 101 | expire = time.Now().Add(app.groupCacheExpire) 102 | } 103 | 104 | return dest.SetBytes(data, expire) 105 | }, 106 | ) 107 | 108 | cache, errGroup := daemon.NewGroup("files", app.groupCacheSizeBytes, getter) 109 | if errGroup != nil { 110 | log.Fatalf("new group: %v", errGroup) 111 | } 112 | 113 | app.cache = cache 114 | } 115 | -------------------------------------------------------------------------------- /examples/kubegroup-example3/main.go: -------------------------------------------------------------------------------- 1 | // Package main implements the example. 2 | package main 3 | 4 | import ( 5 | "context" 6 | "flag" 7 | "log" 8 | "net/http" 9 | "os" 10 | "os/signal" 11 | "strings" 12 | "syscall" 13 | "time" 14 | 15 | "github.com/groupcache/groupcache-go/v3/transport" 16 | "github.com/prometheus/client_golang/prometheus" 17 | "github.com/prometheus/client_golang/prometheus/collectors" 18 | "github.com/prometheus/client_golang/prometheus/promhttp" 19 | "github.com/udhos/kubegroup/kubegroup" 20 | ) 21 | 22 | type application struct { 23 | listenAddr string 24 | groupCachePort string 25 | groupCacheSizeBytes int64 26 | groupCacheExpire time.Duration 27 | 28 | serverMain *http.Server 29 | cache transport.Group 30 | group *kubegroup.Group 31 | 32 | registry *prometheus.Registry 33 | } 34 | 35 | func main() { 36 | 37 | var dogstatsd bool 38 | var prom bool 39 | var mockDogstatsd bool 40 | flag.BoolVar(&dogstatsd, "dogstatsd", true, "enable dogstatsd") 41 | flag.BoolVar(&prom, "prom", true, "enable prometheus") 42 | flag.BoolVar(&mockDogstatsd, "mockDogstatsd", true, "mock dogstatsd") 43 | flag.Parse() 44 | log.Printf("dogstatds=%t prom=%t mockDogstatsd=%t", 45 | dogstatsd, prom, mockDogstatsd) 46 | 47 | mux := http.NewServeMux() 48 | 49 | app := &application{ 50 | listenAddr: ":8080", 51 | groupCachePort: ":5000", 52 | groupCacheSizeBytes: 1_000_000, // limit cache at 1 MB 53 | groupCacheExpire: 60 * time.Second, // cache TTL at 60s 54 | } 55 | 56 | // 57 | // metrics 58 | // 59 | 60 | if prom { 61 | app.registry = prometheus.NewRegistry() 62 | 63 | app.registry.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{})) 64 | app.registry.MustRegister(collectors.NewGoCollector()) 65 | 66 | mux.Handle("/metrics", app.metricsHandler()) 67 | } 68 | 69 | // 70 | // main server 71 | // 72 | 73 | startGroupcache(app, dogstatsd, mockDogstatsd) 74 | 75 | app.serverMain = &http.Server{Addr: app.listenAddr, Handler: mux} 76 | 77 | mux.HandleFunc("/", func(w http.ResponseWriter, 78 | r *http.Request) { 79 | routeHandler(w, r, app) 80 | }) 81 | 82 | go func() { 83 | // 84 | // start main http server 85 | // 86 | log.Printf("main server: listening on %s", app.listenAddr) 87 | err := app.serverMain.ListenAndServe() 88 | log.Printf("main server: exited: %v", err) 89 | }() 90 | 91 | shutdown(app) 92 | 93 | log.Printf("exiting") 94 | } 95 | 96 | func (app *application) metricsHandler() http.Handler { 97 | registerer := app.registry 98 | gatherer := app.registry 99 | return promhttp.InstrumentMetricHandler( 100 | registerer, promhttp.HandlerFor(gatherer, promhttp.HandlerOpts{}), 101 | ) 102 | } 103 | 104 | func routeHandler(w http.ResponseWriter, r *http.Request, app *application) { 105 | 106 | filePath := r.URL.Path 107 | 108 | filePath = strings.TrimPrefix(filePath, "/") 109 | 110 | var data []byte 111 | errGet := app.cache.Get(r.Context(), filePath, transport.AllocatingByteSliceSink(&data)) 112 | if errGet != nil { 113 | log.Printf("routeHandler: %s %s: cache error: %v", r.Method, r.URL.Path, errGet) 114 | http.Error(w, errGet.Error(), 500) 115 | return 116 | } 117 | 118 | if _, errWrite := w.Write(data); errWrite != nil { 119 | log.Printf("routeHandler: %s %s: write error: %v", r.Method, r.URL.Path, errWrite) 120 | } 121 | } 122 | 123 | func shutdown(app *application) { 124 | quit := make(chan os.Signal, 1) 125 | signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) 126 | sig := <-quit 127 | 128 | log.Printf("received signal '%v', initiating shutdown", sig) 129 | 130 | log.Printf("stopping kubegroup") 131 | 132 | app.group.Close() // release kubegroup resources 133 | 134 | time.Sleep(time.Second) // give kubegroup time to log debug messages about exiting 135 | 136 | log.Printf("stopping http servers") 137 | 138 | httpShutdown(app.serverMain) 139 | } 140 | 141 | func httpShutdown(server *http.Server) { 142 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 143 | defer cancel() 144 | if err := server.Shutdown(ctx); err != nil { 145 | log.Printf("http server shutdown error: %v", err) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/udhos/kubegroup 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | github.com/groupcache/groupcache-go/v3 v3.2.0 7 | github.com/modernprogram/groupcache/v2 v2.7.6 8 | github.com/prometheus/client_golang v1.22.0 9 | github.com/udhos/dogstatsdclient v0.0.4 10 | github.com/udhos/kube v1.0.4 11 | github.com/udhos/kubepodinformer v1.0.4 12 | k8s.io/client-go v0.33.0 13 | ) 14 | 15 | require ( 16 | github.com/DataDog/datadog-go/v5 v5.6.0 // indirect 17 | github.com/Microsoft/go-winio v0.6.2 // indirect 18 | github.com/beorn7/perks v1.0.1 // indirect 19 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 20 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 21 | github.com/emicklei/go-restful/v3 v3.12.2 // indirect 22 | github.com/fxamacker/cbor/v2 v2.8.0 // indirect 23 | github.com/go-logr/logr v1.4.2 // indirect 24 | github.com/go-openapi/jsonpointer v0.21.1 // indirect 25 | github.com/go-openapi/jsonreference v0.21.0 // indirect 26 | github.com/go-openapi/swag v0.23.1 // indirect 27 | github.com/gogo/protobuf v1.3.2 // indirect 28 | github.com/google/gnostic-models v0.6.9 // indirect 29 | github.com/google/go-cmp v0.7.0 // indirect 30 | github.com/google/uuid v1.6.0 // indirect 31 | github.com/josharian/intern v1.0.0 // indirect 32 | github.com/json-iterator/go v1.1.12 // indirect 33 | github.com/mailru/easyjson v0.9.0 // indirect 34 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 35 | github.com/modern-go/reflect2 v1.0.2 // indirect 36 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 37 | github.com/pkg/errors v0.9.1 // indirect 38 | github.com/prometheus/client_model v0.6.2 // indirect 39 | github.com/prometheus/common v0.63.0 // indirect 40 | github.com/prometheus/procfs v0.16.1 // indirect 41 | github.com/segmentio/fasthash v1.0.3 // indirect 42 | github.com/spf13/pflag v1.0.6 // indirect 43 | github.com/x448/float16 v0.8.4 // indirect 44 | golang.org/x/net v0.39.0 // indirect 45 | golang.org/x/oauth2 v0.29.0 // indirect 46 | golang.org/x/sys v0.32.0 // indirect 47 | golang.org/x/term v0.31.0 // indirect 48 | golang.org/x/text v0.24.0 // indirect 49 | golang.org/x/time v0.11.0 // indirect 50 | google.golang.org/protobuf v1.36.6 // indirect 51 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 52 | gopkg.in/inf.v0 v0.9.1 // indirect 53 | gopkg.in/yaml.v3 v3.0.1 // indirect 54 | k8s.io/api v0.33.0 // indirect 55 | k8s.io/apimachinery v0.33.0 // indirect 56 | k8s.io/klog/v2 v2.130.1 // indirect 57 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect 58 | k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect 59 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect 60 | sigs.k8s.io/randfill v1.0.0 // indirect 61 | sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect 62 | sigs.k8s.io/yaml v1.4.0 // indirect 63 | ) 64 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/DataDog/datadog-go/v5 v5.6.0 h1:2oCLxjF/4htd55piM75baflj/KoE6VYS7alEUqFvRDw= 2 | github.com/DataDog/datadog-go/v5 v5.6.0/go.mod h1:K9kcYBlxkcPP8tvvjZZKs/m1edNAUFzBbdpTUKfCsuw= 3 | github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= 4 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 5 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 9 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 10 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 13 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= 15 | github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 16 | github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= 17 | github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= 18 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 19 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 20 | github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= 21 | github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= 22 | github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= 23 | github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= 24 | github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= 25 | github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= 26 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 27 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 28 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 29 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 30 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 31 | github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= 32 | github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= 33 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 34 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 35 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 36 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 37 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= 38 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 39 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 40 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 41 | github.com/groupcache/groupcache-go/v3 v3.2.0 h1:WI6adxEvV5nPWYq+2Vj7mQ2KIbQ4VsWHTmdQ7sgqbck= 42 | github.com/groupcache/groupcache-go/v3 v3.2.0/go.mod h1:wIq5yg6mM3Ue4uPs2skO1tvkpOjyNNkXKva9XzToN3c= 43 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 44 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 45 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 46 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 47 | github.com/kapetan-io/tackle v0.10.0 h1:EsNnlIYE8eoGxZhNyI1m91cDGGVDNsOru25V811MS00= 48 | github.com/kapetan-io/tackle v0.10.0/go.mod h1:E7MpdJUog4MvyKkWtQyX8UjFe5tL4SHQ44ZGk+zDBM8= 49 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 50 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 51 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 52 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 53 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 54 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 55 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 56 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 57 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 58 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 59 | github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= 60 | github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= 61 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 62 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 63 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 64 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 65 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 66 | github.com/modernprogram/groupcache/v2 v2.7.6 h1:fkUbVVyg8vGF7XBeZ6CF9+XJoLUSiJvfkQCL/NMOIfU= 67 | github.com/modernprogram/groupcache/v2 v2.7.6/go.mod h1:0t2HiBuXCy1TGoHTT2/nKYtnXmnKBR1671v9FL52OGg= 68 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 69 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 70 | github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= 71 | github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= 72 | github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= 73 | github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= 74 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 75 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 76 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 77 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 78 | github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= 79 | github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 80 | github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 81 | github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 82 | github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= 83 | github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= 84 | github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= 85 | github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= 86 | github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= 87 | github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 88 | github.com/segmentio/fasthash v1.0.3 h1:EI9+KE1EwvMLBWwjpRDc+fEM+prwxDYbslddQGtrmhM= 89 | github.com/segmentio/fasthash v1.0.3/go.mod h1:waKX8l2N8yckOgmSsXJi7x1ZfdKZ4x7KRMzBtS3oedY= 90 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 91 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 92 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 93 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 94 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 95 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 96 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 97 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 98 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 99 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 100 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 101 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 102 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 103 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 104 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 105 | github.com/udhos/dogstatsdclient v0.0.4 h1:IgiMr+82i7TOtVDpnr9Z7Szcqz6MeQZynhgbBk97bgA= 106 | github.com/udhos/dogstatsdclient v0.0.4/go.mod h1:SGlnVGLUAMdf+MeXX3XbGvTu9L/vbPL+An87SeXCgbs= 107 | github.com/udhos/kube v1.0.4 h1:K29UtnX5p+AjA8RcPfB3Y3hyHSlbJJp8af94yNq+0lw= 108 | github.com/udhos/kube v1.0.4/go.mod h1:MwPicQML0gvzFH+svkM5WyRaDcgstKNzUBTNFME9NoI= 109 | github.com/udhos/kubepodinformer v1.0.4 h1:xC+7W4Od+C+cnU/xf2Qxm5O+72fyjFtdiduK5G7hi20= 110 | github.com/udhos/kubepodinformer v1.0.4/go.mod h1:zpMwmiESpVOdckcL1cNxil+mq1SVcnEoIKtHrljNcag= 111 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 112 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 113 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 114 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 115 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 116 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 117 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 118 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 119 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 120 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 121 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 122 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 123 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 124 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 125 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 126 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 127 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 128 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 129 | golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= 130 | golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 131 | golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= 132 | golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 133 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 134 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 135 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 136 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 137 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 138 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 139 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 140 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 141 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 142 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 143 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 144 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 145 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 146 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 147 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 148 | golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= 149 | golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= 150 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 151 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 152 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 153 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 154 | golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= 155 | golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 156 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 157 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 158 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 159 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 160 | golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 161 | golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= 162 | golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= 163 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 164 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 165 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 166 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 167 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 168 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 169 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 170 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 171 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 172 | gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= 173 | gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 174 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 175 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 176 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 177 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 178 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 179 | k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU= 180 | k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM= 181 | k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ= 182 | k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= 183 | k8s.io/client-go v0.33.0 h1:UASR0sAYVUzs2kYuKn/ZakZlcs2bEHaizrrHUZg0G98= 184 | k8s.io/client-go v0.33.0/go.mod h1:kGkd+l/gNGg8GYWAPr0xF1rRKvVWvzh9vmZAMXtaKOg= 185 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 186 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 187 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= 188 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= 189 | k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e h1:KqK5c/ghOm8xkHYhlodbp6i6+r+ChV2vuAuVRdFbLro= 190 | k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 191 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= 192 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= 193 | sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 194 | sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= 195 | sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 196 | sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI= 197 | sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= 198 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 199 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 200 | -------------------------------------------------------------------------------- /kubegroup/kubegroup.go: -------------------------------------------------------------------------------- 1 | // Package kubegroup provides autodiscovery for groupcache. 2 | package kubegroup 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | "log" 8 | "net" 9 | "os" 10 | 11 | "github.com/groupcache/groupcache-go/v3/transport/peer" 12 | "github.com/prometheus/client_golang/prometheus" 13 | "github.com/udhos/kubepodinformer/podinformer" 14 | "k8s.io/client-go/kubernetes" 15 | ) 16 | 17 | // FindMyURL returns my URL for groupcache pool. 18 | // groupcachePort example: ":5000". 19 | // Sample resulting URL: "http://10.0.0.1:5000" 20 | func FindMyURL(groupcachePort string) (string, error) { 21 | addr, errAddr := findMyAddr() 22 | if errAddr != nil { 23 | return "", errAddr 24 | } 25 | url := buildURL(addr, groupcachePort) 26 | return url, nil 27 | } 28 | 29 | // FindMyAddress returns my address. 30 | func FindMyAddress() (string, error) { 31 | return findMyAddr() 32 | } 33 | 34 | func findMyAddr() (string, error) { 35 | host, errHost := os.Hostname() 36 | if errHost != nil { 37 | return "", errHost 38 | } 39 | addrs, errAddr := net.LookupHost(host) 40 | if errAddr != nil { 41 | return "", errAddr 42 | } 43 | if len(addrs) < 1 { 44 | return "", fmt.Errorf("findMyAddr: hostname '%s': no addr found", host) 45 | } 46 | addr := addrs[0] 47 | if len(addrs) > 1 { 48 | return addr, fmt.Errorf("findMyAddr: hostname '%s': found multiple addresses: %v", host, addrs) 49 | } 50 | return addr, nil 51 | } 52 | 53 | func findMyNamespace() (string, error) { 54 | buf, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace") 55 | return string(buf), err 56 | } 57 | 58 | func buildURL(addr, groupcachePort string) string { 59 | return "http://" + addr + groupcachePort 60 | } 61 | 62 | // PeerGroup is an interface to plug in a target for delivering peering 63 | // updates. *groupcache.HTTPPool, created with 64 | // groupcache.NewHTTPPoolOpts(), implements this interface. 65 | type PeerGroup interface { 66 | Set(peers ...string) 67 | } 68 | 69 | // PeerSet is an interface to plug in a target for delivering peering 70 | // updates. *groupcache.daemon, created with 71 | // groupcache.ListenAndServe(), implements this interface. 72 | type PeerSet interface { 73 | SetPeers(ctx context.Context, peers []peer.Info) error 74 | } 75 | 76 | // Options specifies options for UpdatePeers. 77 | type Options struct { 78 | // Pool is an interface to plug in a target for delivering peering 79 | // updates. *groupcache.HTTPPool, created with 80 | // groupcache.NewHTTPPoolOpts(), implements this interface. 81 | // Pool supports groupcache2. 82 | Pool PeerGroup 83 | 84 | // Peers is an interface to plug in a target for delivering peering 85 | // updates. *groupcache.Daemon, created with 86 | // groupcache.ListenAndServe(), implements this interface. 87 | // Peers supports groupcache3. 88 | Peers PeerSet 89 | 90 | // Client provides kubernetes client. 91 | Client *kubernetes.Clientset 92 | 93 | // GroupCachePort is the listening port used by groupcache peering http 94 | // server. For instance, ":5000". 95 | GroupCachePort string 96 | 97 | // LabelSelector is required. Example: "key1=value1,key2=value2" 98 | LabelSelector string 99 | 100 | // Debug enables non-error logging. Errors are always logged. 101 | Debug bool 102 | 103 | // Logf optionally sets custom logging. 104 | Logf func(format string, v ...any) 105 | 106 | // MetricsNamespace provides optional namespace for prometheus metrics. 107 | MetricsNamespace string 108 | 109 | // MetricsRegisterer is registerer for prometheus metrics. 110 | MetricsRegisterer prometheus.Registerer 111 | 112 | // MetricsRegisterer is gatherer for prometheus metrics. 113 | MetricsGatherer prometheus.Gatherer 114 | 115 | // DogstatsdClient optionally sends metrics to Datadog Dogstatsd. 116 | DogstatsdClient DogstatsdClient 117 | 118 | DogstatsdExtraTags []string 119 | 120 | // DogstatsdTagHosnameKey defaults to "pod_name". 121 | DogstatsdTagHosnameKey string 122 | 123 | // DogstatsdDisableTagHostname prevents adding tag $DogstatsdTagHosnameKey:$hostname 124 | DogstatsdDisableTagHostname bool 125 | 126 | // ForceNamespaceDefault is used only for testing. 127 | ForceNamespaceDefault bool 128 | } 129 | 130 | // DogstatsdClient is implemented by *statsd.Client. 131 | // Simplified version of statsd.ClientInterface. 132 | type DogstatsdClient interface { 133 | // Gauge measures the value of a metric at a particular time. 134 | Gauge(name string, value float64, tags []string, rate float64) error 135 | 136 | // Count tracks how many times something happened per second. 137 | Count(name string, value int64, tags []string, rate float64) error 138 | 139 | // Close the client connection. 140 | Close() error 141 | } 142 | 143 | // Group holds context for kubegroup. 144 | type Group struct { 145 | options Options 146 | informer *podinformer.PodInformer 147 | m *metrics 148 | myAddr string 149 | } 150 | 151 | func (g *Group) debugf(format string, v ...any) { 152 | if g.options.Debug { 153 | g.options.Logf("DEBUG kubegroup: "+format, v...) 154 | } 155 | } 156 | 157 | func (g *Group) errorf(format string, v ...any) { 158 | g.options.Logf("ERROR kubegroup: "+format, v...) 159 | } 160 | 161 | // Close terminates kubegroup goroutines to release resources. 162 | func (g *Group) Close() { 163 | g.debugf("Close called to release resources") 164 | g.informer.Stop() 165 | } 166 | 167 | // UpdatePeers continuously updates groupcache peers. 168 | func UpdatePeers(options Options) (*Group, error) { 169 | 170 | // 171 | // Required fields. 172 | // 173 | if options.Pool == nil && options.Peers == nil { 174 | panic("Pool and Peers are both nil") 175 | } 176 | if options.Client == nil { 177 | panic("Client is nil") 178 | } 179 | if options.GroupCachePort == "" { 180 | panic("GroupCachePort is empty") 181 | } 182 | if options.LabelSelector == "" { 183 | panic("LabelSelector is empty") 184 | } 185 | 186 | if !options.DogstatsdDisableTagHostname { 187 | if options.DogstatsdTagHosnameKey == "" { 188 | options.DogstatsdTagHosnameKey = "pod_name" 189 | } 190 | hostname, err := os.Hostname() 191 | if err != nil { 192 | return nil, err 193 | } 194 | options.DogstatsdExtraTags = append(options.DogstatsdExtraTags, 195 | fmt.Sprintf("%s:%s", options.DogstatsdTagHosnameKey, hostname)) 196 | } 197 | 198 | if options.Logf == nil { 199 | options.Logf = log.Printf 200 | } 201 | 202 | var namespace string 203 | if options.ForceNamespaceDefault { 204 | namespace = "default" 205 | } else { 206 | ns, errNs := findMyNamespace() 207 | if errNs != nil { 208 | return nil, errNs 209 | } 210 | namespace = ns 211 | } 212 | 213 | myAddr, errAddr := findMyAddr() 214 | if errAddr != nil { 215 | return nil, errAddr 216 | } 217 | 218 | group := &Group{ 219 | options: options, 220 | m: newMetrics(options.MetricsNamespace, 221 | options.MetricsRegisterer, options.DogstatsdClient, 222 | options.DogstatsdExtraTags), 223 | myAddr: myAddr, 224 | } 225 | 226 | optionsInformer := podinformer.Options{ 227 | Client: options.Client, 228 | Namespace: namespace, 229 | LabelSelector: options.LabelSelector, 230 | OnUpdate: group.onUpdate, 231 | DebugLog: options.Debug, 232 | } 233 | 234 | group.informer = podinformer.New(optionsInformer) 235 | 236 | go func() { 237 | errInformer := group.informer.Run() 238 | group.errorf("informer exited, error: %v", errInformer) 239 | }() 240 | 241 | return group, nil 242 | } 243 | 244 | func (g *Group) onUpdate(pods []podinformer.Pod) { 245 | const me = "onUpdate" 246 | 247 | size := len(pods) 248 | g.debugf("%s: %d", me, size) 249 | 250 | if g.options.Peers != nil { 251 | 252 | // 253 | // groupcache3 254 | // 255 | 256 | peers := make([]peer.Info, 0, size) 257 | 258 | for i, p := range pods { 259 | hostPort := p.IP + g.options.GroupCachePort 260 | isSelf := g.myAddr == p.IP 261 | 262 | g.debugf("%s: %d/%d: namespace=%s pod=%s ip=%s ready=%t host_port=%s is_self=%t", 263 | me, i+1, size, p.Namespace, p.Name, p.IP, p.Ready, hostPort, isSelf) 264 | 265 | if p.Ready { 266 | peers = append(peers, peer.Info{ 267 | Address: hostPort, 268 | IsSelf: isSelf, 269 | }) 270 | } 271 | } 272 | 273 | err := g.options.Peers.SetPeers(context.TODO(), peers) 274 | if err != nil { 275 | g.errorf("set peers: error: %v", err) 276 | } 277 | 278 | } else { 279 | 280 | // 281 | // groupcache2 282 | // 283 | 284 | peers := make([]string, 0, size) 285 | 286 | for i, p := range pods { 287 | g.debugf("%s: %d/%d: namespace=%s pod=%s ip=%s ready=%t", 288 | me, i+1, size, p.Namespace, p.Name, p.IP, p.Ready) 289 | 290 | if p.Ready { 291 | peers = append(peers, buildURL(p.IP, g.options.GroupCachePort)) 292 | } 293 | } 294 | 295 | g.options.Pool.Set(peers...) 296 | } 297 | 298 | g.m.update(size) 299 | } 300 | 301 | // DogstatsdClientMock mocks the interface DogstatsdClient. 302 | type DogstatsdClientMock struct{} 303 | 304 | // Gauge measures the value of a metric at a particular time. 305 | func (m *DogstatsdClientMock) Gauge(name string, value float64, tags []string, rate float64) error { 306 | log.Printf("DogstatsdClientMock.Gauge: name=%s value=%f tags=%v rate=%f", 307 | name, value, tags, rate) 308 | return nil 309 | } 310 | 311 | // Count tracks how many times something happened per second. 312 | func (m *DogstatsdClientMock) Count(name string, value int64, tags []string, rate float64) error { 313 | log.Printf("DogstatsdClientMock.Count: name=%s value=%d tags=%v rate=%f", 314 | name, value, tags, rate) 315 | return nil 316 | } 317 | 318 | // Close the client connection. 319 | func (m *DogstatsdClientMock) Close() error { 320 | return nil 321 | } 322 | -------------------------------------------------------------------------------- /kubegroup/metrics.go: -------------------------------------------------------------------------------- 1 | package kubegroup 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | 7 | "github.com/prometheus/client_golang/prometheus" 8 | "github.com/prometheus/client_golang/prometheus/promauto" 9 | ) 10 | 11 | type metrics struct { 12 | peers prometheus.Gauge 13 | events prometheus.Counter 14 | dogstatsdClient DogstatsdClient 15 | sampleRate float64 16 | tags []string 17 | } 18 | 19 | func (m *metrics) update(peers int) { 20 | if m.events != nil { 21 | m.events.Inc() 22 | } 23 | if m.peers != nil { 24 | m.peers.Set(float64(peers)) 25 | } 26 | 27 | if m.dogstatsdClient != nil { 28 | if err := m.dogstatsdClient.Count("events", 1, m.tags, m.sampleRate); err != nil { 29 | slog.Error(fmt.Sprintf("exportCount: error: %v", err)) 30 | } 31 | if err := m.dogstatsdClient.Gauge("peers", float64(peers), m.tags, m.sampleRate); err != nil { 32 | slog.Error(fmt.Sprintf("exportGauge: error: %v", err)) 33 | } 34 | } 35 | } 36 | 37 | func newMetrics(namespace string, registerer prometheus.Registerer, 38 | client DogstatsdClient, dogstatsdExtraTags []string) *metrics { 39 | 40 | m := &metrics{ 41 | dogstatsdClient: client, 42 | tags: dogstatsdExtraTags, 43 | sampleRate: 1, 44 | } 45 | 46 | if registerer == nil { 47 | return m 48 | } 49 | 50 | const subsystem = "kubegroup" 51 | 52 | m.peers = newGauge( 53 | registerer, 54 | prometheus.GaugeOpts{ 55 | Namespace: namespace, 56 | Subsystem: subsystem, 57 | Name: "peers", 58 | Help: "Number of peer PODs discovered.", 59 | }, 60 | ) 61 | 62 | m.events = newCounter( 63 | registerer, 64 | prometheus.CounterOpts{ 65 | Namespace: namespace, 66 | Subsystem: subsystem, 67 | Name: "events", 68 | Help: "Number of events received.", 69 | }, 70 | ) 71 | 72 | return m 73 | } 74 | 75 | func newGauge(registerer prometheus.Registerer, 76 | opts prometheus.GaugeOpts) prometheus.Gauge { 77 | return promauto.With(registerer).NewGauge(opts) 78 | } 79 | 80 | func newCounter(registerer prometheus.Registerer, 81 | opts prometheus.CounterOpts) prometheus.Counter { 82 | return promauto.With(registerer).NewCounter(opts) 83 | } 84 | --------------------------------------------------------------------------------