diff --git a/deploy/helm/kerno/templates/clusterrole.yaml b/deploy/helm/kerno/templates/clusterrole.yaml index 2ad69b5..a563836 100644 --- a/deploy/helm/kerno/templates/clusterrole.yaml +++ b/deploy/helm/kerno/templates/clusterrole.yaml @@ -5,17 +5,9 @@ metadata: labels: {{- include "kerno.labels" . | nindent 4 }} rules: - # ── Pod/Node enrichment (cgroup → pod name, namespace, labels) ────── + # ── Pod enrichment (cgroup → pod name, namespace, labels) ─────────── - apiGroups: [""] - resources: ["pods", "nodes"] - verbs: ["get", "list", "watch"] - # ── Namespace enrichment ──────────────────────────────────────────── - - apiGroups: [""] - resources: ["namespaces"] - verbs: ["get", "list"] - # ── Service name resolution (pod labels → Service name mapping) ───── - - apiGroups: [""] - resources: ["services", "endpoints"] + resources: ["pods"] verbs: ["get", "list", "watch"] --- apiVersion: rbac.authorization.k8s.io/v1 diff --git a/go.mod b/go.mod index 5a99750..6540c62 100644 --- a/go.mod +++ b/go.mod @@ -8,16 +8,38 @@ require ( github.com/spf13/cobra v1.10.2 github.com/spf13/viper v1.21.0 golang.org/x/sys v0.45.0 + k8s.io/api v0.31.0 + k8s.io/apimachinery v0.31.0 + k8s.io/client-go v0.31.0 ) require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.23.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/imdario/mergo v0.3.6 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/kylelemons/godebug v1.1.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/prometheus/client_model v0.6.2 // indirect @@ -30,8 +52,22 @@ require ( github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/subosito/gotenv v1.6.0 // indirect - go.yaml.in/yaml/v2 v2.4.2 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/text v0.28.0 // indirect - google.golang.org/protobuf v1.36.8 // indirect + golang.org/x/net v0.49.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/term v0.39.0 // indirect + golang.org/x/text v0.33.0 // indirect + golang.org/x/time v0.14.0 // indirect + google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/klog/v2 v2.140.0 // indirect + k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect + k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index 2e5ce79..bc91de4 100644 --- a/go.sum +++ b/go.sum @@ -6,40 +6,97 @@ github.com/cilium/ebpf v0.21.0 h1:4dpx1J/B/1apeTmWBH5BkVLayHTkFrMovVPnHEk+l3k= github.com/cilium/ebpf v0.21.0/go.mod h1:1kHKv6Kvh5a6TePP5vvvoMa1bclRyzUXELSs272fmIQ= github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6 h1:teYtXy9B7y5lHTp8V9KPxpYRAVA7dozigQcMiBust1s= github.com/go-quicktest/qt v1.101.1-0.20240301121107-c6c8733fa1e6/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af h1:kmjWCqn2qkEml422C2Rrd27c3VGxi6a/6HNq8QmHRKM= +github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= +github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCXxpaIEIlM= github.com/jsimonetti/rtnetlink/v2 v2.0.1/go.mod h1:7MoNYNbb3UaDHtF8udiJo/RH6VsTKP1pqKLUTVCvToE= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= +github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= +github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= @@ -69,28 +126,96 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= -go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= -golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= -google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= -google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.31.0 h1:b9LiSjR2ym/SzTOlfMHm1tr7/21aD7fSkqgD/CVJBCo= +k8s.io/api v0.31.0/go.mod h1:0YiFF+JfFxMM6+1hQei8FY8M7s1Mth+z/q7eF1aJkTE= +k8s.io/apimachinery v0.31.0 h1:m9jOiSr3FoSSL5WO9bjm1n6B9KROYYgNZOb4tyZ1lBc= +k8s.io/apimachinery v0.31.0/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/client-go v0.31.0 h1:QqEJzNjbN2Yv1H79SsS+SWnXkBgVu4Pj3CJQgbx0gI8= +k8s.io/client-go v0.31.0/go.mod h1:Y9wvC76g4fLjmU0BA+rV+h2cncoadjvjjkkIGoTLcGU= +k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= +k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/internal/adapter/adapter_test.go b/internal/adapter/adapter_test.go index 2b25158..04683d5 100644 --- a/internal/adapter/adapter_test.go +++ b/internal/adapter/adapter_test.go @@ -5,10 +5,7 @@ package adapter import ( "context" - "encoding/json" "log/slog" - "net/http" - "net/http/httptest" "testing" ) @@ -237,76 +234,11 @@ func TestIsUIDLike(t *testing.T) { } } -func TestKubernetesAdapter_FetchPods(t *testing.T) { - // Mock Kubelet API. - podList := kubeletPodList{ - Items: []kubeletPod{ - { - Metadata: kubeletMeta{ - Name: "nginx-abc-123", - Namespace: "default", - UID: "12345678-1234-1234-1234-123456789abc", - OwnerReferences: []kubeletOwnerRef{ - {Kind: "ReplicaSet", Name: "nginx-abc"}, - }, - }, - Spec: kubeletSpec{NodeName: "worker-1"}, - }, - { - Metadata: kubeletMeta{ - Name: "redis-0", - Namespace: "cache", - UID: "aaaabbbb-cccc-dddd-eeee-ffffffffffff", - Labels: map[string]string{"app": "redis"}, - }, - Spec: kubeletSpec{NodeName: "worker-1"}, - }, - }, - } - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/pods" { - http.NotFound(w, r) - return - } - json.NewEncoder(w).Encode(podList) - })) - defer server.Close() - - a := NewKubernetesAdapter(slog.Default()) - a.kubeletURL = server.URL - - pods, err := a.fetchPods(context.Background()) - if err != nil { - t.Fatalf("fetchPods() error: %v", err) - } - - if len(pods) != 2 { - t.Fatalf("expected 2 pods, got %d", len(pods)) - } - - // Check first pod (nginx with ReplicaSet owner). - if pods[0].Name != "nginx-abc-123" { - t.Errorf("pods[0].Name = %q, want %q", pods[0].Name, "nginx-abc-123") - } - if pods[0].Deployment != "nginx" { - t.Errorf("pods[0].Deployment = %q, want %q", pods[0].Deployment, "nginx") - } - if pods[0].Namespace != "default" { - t.Errorf("pods[0].Namespace = %q, want %q", pods[0].Namespace, "default") - } - - // Check second pod (redis with app label). - if pods[1].Deployment != "redis" { - t.Errorf("pods[1].Deployment = %q, want %q", pods[1].Deployment, "redis") - } -} - func TestKubernetesAdapter_Enrich(t *testing.T) { a := NewKubernetesAdapter(slog.Default()) - // Manually populate the index. - a.index["12345678-1234-1234-1234-123456789abc"] = &PodInfo{ + // Populate the index directly (same package access). + a.uidIndex["12345678-1234-1234-1234-123456789abc"] = &PodInfo{ Name: "nginx-abc-123", Namespace: "default", Node: "worker-1", diff --git a/internal/adapter/kubernetes.go b/internal/adapter/kubernetes.go index 44a0e9e..59f102e 100644 --- a/internal/adapter/kubernetes.go +++ b/internal/adapter/kubernetes.go @@ -5,37 +5,45 @@ package adapter import ( "context" - "encoding/json" "fmt" "log/slog" - "net/http" "os" "strings" "sync" + "sync/atomic" "time" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/clientcmd" ) -// KubernetesAdapter enriches events with pod metadata by mapping -// cgroup paths to pod names/namespaces via the Kubelet read-only API. +// KubernetesAdapter enriches events with pod metadata using a SharedIndexInformer +// scoped to the local node via spec.nodeName field selector. // // Architecture: -// - On Start(), it fetches the pod list from the local Kubelet API -// and builds a cgroup-to-pod index (map[string]*PodInfo). -// - The index is refreshed periodically (default 30s) to track new pods. -// - Enrich() looks up the PID's cgroup path in the index. -// - No dependency on client-go — uses raw HTTP to the Kubelet. +// - Start() launches a goroutine that builds a clientset (in-cluster auth, +// kubeconfig fallback) and runs a SharedIndexInformer filtered to the node. +// - The informer populates two in-memory indexes: uidIndex and cgroupIndex. +// - If the API server is unreachable the goroutine retries with exponential +// backoff; stale index entries are kept so enrichment continues degraded. +// - IsReady() returns true once the initial cache sync completes. type KubernetesAdapter struct { logger *slog.Logger - hostname string nodeName string + hostname string - mu sync.RWMutex - index map[string]*PodInfo // cgroup path suffix → pod info + mu sync.RWMutex + uidIndex map[string]*PodInfo // pod UID → PodInfo + cgroupIndex map[string]*PodInfo // cgroup fragment → PodInfo - client *http.Client - kubeletURL string - refreshRate time.Duration - cancelFn context.CancelFunc + ready atomic.Bool + cancelFn context.CancelFunc } // PodInfo holds the K8s metadata extracted for a pod. @@ -47,46 +55,36 @@ type PodInfo struct { UID string `json:"uid"` } -// NewKubernetesAdapter creates a Kubernetes adapter. +// NewKubernetesAdapter creates a Kubernetes adapter. It reads NODE_NAME from +// the environment (Downward API injection), falling back to KERNO_NODE_NAME, +// then os.Hostname(). func NewKubernetesAdapter(logger *slog.Logger) *KubernetesAdapter { hostname, _ := os.Hostname() - nodeName := os.Getenv("KERNO_NODE_NAME") + nodeName := os.Getenv("NODE_NAME") + if nodeName == "" { + nodeName = os.Getenv("KERNO_NODE_NAME") + } if nodeName == "" { nodeName = hostname } - return &KubernetesAdapter{ logger: logger, - hostname: hostname, nodeName: nodeName, - index: make(map[string]*PodInfo), - client: &http.Client{Timeout: 5 * time.Second}, - kubeletURL: kubeletReadOnlyURL(), - refreshRate: 30 * time.Second, + hostname: hostname, + uidIndex: make(map[string]*PodInfo), + cgroupIndex: make(map[string]*PodInfo), } } func (a *KubernetesAdapter) Name() string { return "kubernetes" } -func (a *KubernetesAdapter) Start(ctx context.Context) error { - a.logger.Info("kubernetes adapter starting", - "node", a.nodeName, - "kubeletURL", a.kubeletURL, - ) +// IsReady returns true once the informer cache has completed its initial sync. +func (a *KubernetesAdapter) IsReady() bool { return a.ready.Load() } +func (a *KubernetesAdapter) Start(ctx context.Context) error { ctx, cancel := context.WithCancel(ctx) - - // Initial index build with retries. - if err := a.refreshIndex(ctx); err != nil { - a.logger.Warn("initial pod index build failed, will retry", - "error", err, - ) - } a.cancelFn = cancel - - go a.refreshLoop(ctx) - - a.logger.Info("kubernetes adapter started", "pods", len(a.index)) + go a.runWithBackoff(ctx) return nil } @@ -96,198 +94,290 @@ func (a *KubernetesAdapter) Stop() { } } -// LookupByPath resolves a cgroup path to a Kubernetes pod name and namespace. -// Returns ("", "") when the path cannot be matched (non-K8s or unknown pod). -// Implements collector.PodLookup. -func (a *KubernetesAdapter) LookupByPath(cgroupPath string) (pod, namespace string) { - uid := extractPodUID(cgroupPath) - if uid == "" { - return "", "" - } - a.mu.RLock() - info, ok := a.index[uid] - a.mu.RUnlock() - if !ok { - return "", "" +func (a *KubernetesAdapter) runWithBackoff(ctx context.Context) { + backoff := time.Second + const maxBackoff = 2 * time.Minute + + for { + if err := a.run(ctx); err != nil { + if ctx.Err() != nil { + return + } + a.ready.Store(false) + a.logger.Warn("kubernetes informer stopped, retrying", + "error", err, + "backoff", backoff, + ) + select { + case <-ctx.Done(): + return + case <-time.After(backoff): + } + backoff = min(backoff*2, maxBackoff) + } else { + return + } } - return info.Name, info.Namespace } -// Enrich maps the PID's cgroup path to a K8s pod and populates metadata. -func (a *KubernetesAdapter) Enrich(meta *EventMeta) { - meta.Hostname = a.hostname - meta.Node = a.nodeName - - if meta.PID > 0 && meta.CgroupPath == "" { - meta.CgroupPath = cgroupPathForPID(meta.PID) - } - if meta.CgroupPath == "" { - return +func (a *KubernetesAdapter) run(ctx context.Context) error { + cs, err := buildClientset() + if err != nil { + return fmt.Errorf("build clientset: %w", err) } - // Extract pod UID from cgroup path. - uid := extractPodUID(meta.CgroupPath) - if uid == "" { - return - } + fieldSel := fields.OneTermEqualSelector("spec.nodeName", a.nodeName).String() + factory := informers.NewSharedInformerFactoryWithOptions( + cs, + 0, + informers.WithTweakListOptions(func(opts *metav1.ListOptions) { + opts.FieldSelector = fieldSel + }), + ) - a.mu.RLock() - info, ok := a.index[uid] - a.mu.RUnlock() + podInformer := factory.Core().V1().Pods().Informer() + podInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ //nolint:errcheck + AddFunc: func(obj any) { + if pod, ok := obj.(*corev1.Pod); ok { + a.addPod(pod) + } + }, + UpdateFunc: func(old, updated any) { + if oldPod, ok := old.(*corev1.Pod); ok { + a.removePod(oldPod) + } + if pod, ok := updated.(*corev1.Pod); ok { + a.addPod(pod) + } + }, + DeleteFunc: func(obj any) { + pod, ok := obj.(*corev1.Pod) + if !ok { + ts, ok := obj.(cache.DeletedFinalStateUnknown) + if !ok { + return + } + pod, ok = ts.Obj.(*corev1.Pod) + if !ok { + return + } + } + a.removePod(pod) + }, + }) - if ok { - meta.Pod = info.Name - meta.Namespace = info.Namespace - meta.Node = info.Node - meta.Deployment = info.Deployment - } -} + stopCh := make(chan struct{}) + go func() { + <-ctx.Done() + close(stopCh) + }() -// refreshLoop periodically refreshes the pod index. -func (a *KubernetesAdapter) refreshLoop(ctx context.Context) { - ticker := time.NewTicker(a.refreshRate) - defer ticker.Stop() + factory.Start(stopCh) + a.logger.Info("kubernetes informer starting", "node", a.nodeName) - for { + if !cache.WaitForCacheSync(stopCh, podInformer.HasSynced) { select { - case <-ctx.Done(): - return - case <-ticker.C: - if err := a.refreshIndex(ctx); err != nil { - a.logger.Debug("pod index refresh failed", "error", err) - } + case <-stopCh: + return nil // context canceled — clean shutdown + default: + return fmt.Errorf("informer cache sync failed") } } + + a.ready.Store(true) + a.logger.Info("kubernetes informer synced", "node", a.nodeName) + + <-stopCh + return nil } -// refreshIndex fetches the pod list from Kubelet and rebuilds the index. -func (a *KubernetesAdapter) refreshIndex(ctx context.Context) error { - pods, err := a.fetchPods(ctx) +// buildClientset constructs a clientset using in-cluster auth when available, +// falling back to KUBECONFIG / ~/.kube/config for local development. +func buildClientset() (*kubernetes.Clientset, error) { + cfg, err := rest.InClusterConfig() if err != nil { - return err + kubeconfig := os.Getenv("KUBECONFIG") + if kubeconfig == "" { + if home, herr := os.UserHomeDir(); herr == nil { + kubeconfig = home + "/.kube/config" + } + } + cfg, err = clientcmd.BuildConfigFromFlags("", kubeconfig) + if err != nil { + return nil, fmt.Errorf("kubeconfig: %w", err) + } } + return kubernetes.NewForConfig(cfg) +} - newIndex := make(map[string]*PodInfo, len(pods)) - for _, pod := range pods { - newIndex[pod.UID] = pod - } +// addPod inserts or replaces the index entries for a pod. +func (a *KubernetesAdapter) addPod(pod *corev1.Pod) { + info := podInfoFromK8s(pod) + uid := string(pod.UID) a.mu.Lock() - a.index = newIndex - a.mu.Unlock() + defer a.mu.Unlock() - a.logger.Debug("pod index refreshed", "pods", len(newIndex)) - return nil -} + a.uidIndex[uid] = info + a.cgroupIndex[uid] = info + // Systemd cgroup v2 driver escapes dashes to underscores. + a.cgroupIndex[strings.ReplaceAll(uid, "-", "_")] = info -// kubeletPodList is the minimal structure for Kubelet /pods response. -type kubeletPodList struct { - Items []kubeletPod `json:"items"` + for _, cs := range pod.Status.ContainerStatuses { + a.indexContainerID(cs.ContainerID, info) + } + for _, cs := range pod.Status.InitContainerStatuses { + a.indexContainerID(cs.ContainerID, info) + } } -type kubeletPod struct { - Metadata kubeletMeta `json:"metadata"` - Spec kubeletSpec `json:"spec"` -} +// removePod deletes all index entries for a pod. +func (a *KubernetesAdapter) removePod(pod *corev1.Pod) { + uid := string(pod.UID) -type kubeletMeta struct { - Name string `json:"name"` - Namespace string `json:"namespace"` - UID string `json:"uid"` - OwnerReferences []kubeletOwnerRef `json:"ownerReferences,omitempty"` - Labels map[string]string `json:"labels,omitempty"` -} + a.mu.Lock() + defer a.mu.Unlock() -type kubeletSpec struct { - NodeName string `json:"nodeName"` -} + delete(a.uidIndex, uid) + delete(a.cgroupIndex, uid) + delete(a.cgroupIndex, strings.ReplaceAll(uid, "-", "_")) -type kubeletOwnerRef struct { - Kind string `json:"kind"` - Name string `json:"name"` + for _, cs := range pod.Status.ContainerStatuses { + a.unindexContainerID(cs.ContainerID) + } + for _, cs := range pod.Status.InitContainerStatuses { + a.unindexContainerID(cs.ContainerID) + } } -// fetchPods retrieves the pod list from the Kubelet read-only API. -func (a *KubernetesAdapter) fetchPods(ctx context.Context) ([]*PodInfo, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, a.kubeletURL+"/pods", nil) - if err != nil { - return nil, fmt.Errorf("kubelet request build: %w", err) +// indexContainerID adds container ID entries to cgroupIndex. Must be called +// with a.mu held. +func (a *KubernetesAdapter) indexContainerID(ref string, info *PodInfo) { + cid := stripRuntimePrefix(ref) + if cid == "" { + return } - resp, err := a.client.Do(req) - if err != nil { - return nil, fmt.Errorf("kubelet request: %w", err) + a.cgroupIndex[cid] = info + if len(cid) >= 12 { + a.cgroupIndex[cid[:12]] = info } - defer resp.Body.Close() +} - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("kubelet returned %d", resp.StatusCode) +// unindexContainerID removes container ID entries from cgroupIndex. Must be +// called with a.mu held. +func (a *KubernetesAdapter) unindexContainerID(ref string) { + cid := stripRuntimePrefix(ref) + if cid == "" { + return } - - var podList kubeletPodList - if err := json.NewDecoder(resp.Body).Decode(&podList); err != nil { - return nil, fmt.Errorf("decoding kubelet response: %w", err) + delete(a.cgroupIndex, cid) + if len(cid) >= 12 { + delete(a.cgroupIndex, cid[:12]) } +} - pods := make([]*PodInfo, 0, len(podList.Items)) - for _, item := range podList.Items { - info := &PodInfo{ - Name: item.Metadata.Name, - Namespace: item.Metadata.Namespace, - Node: item.Spec.NodeName, - UID: item.Metadata.UID, - } +// stripRuntimePrefix strips the "containerd://", "docker://", etc. prefix +// from a container ID reference. +func stripRuntimePrefix(ref string) string { + if i := strings.LastIndex(ref, "//"); i >= 0 { + return ref[i+2:] + } + return ref +} - // Extract deployment name from ownerReferences or labels. - for _, ref := range item.Metadata.OwnerReferences { - if ref.Kind == "ReplicaSet" { - // ReplicaSet name is typically -. - info.Deployment = extractDeploymentName(ref.Name) - break - } +func podInfoFromK8s(pod *corev1.Pod) *PodInfo { + info := &PodInfo{ + Name: pod.Name, + Namespace: pod.Namespace, + Node: pod.Spec.NodeName, + UID: string(pod.UID), + } + for _, ref := range pod.OwnerReferences { + if ref.Kind == "ReplicaSet" { + info.Deployment = extractDeploymentName(ref.Name) + break } - if info.Deployment == "" { - if d, ok := item.Metadata.Labels["app"]; ok { - info.Deployment = d - } + } + if info.Deployment == "" { + if d, ok := pod.Labels["app"]; ok { + info.Deployment = d } - - pods = append(pods, info) } + return info +} - return pods, nil +// LookupByUID returns the PodInfo for the given pod UID, or nil if not found. +func (a *KubernetesAdapter) LookupByUID(uid string) *PodInfo { + a.mu.RLock() + info := a.uidIndex[uid] + a.mu.RUnlock() + return info +} + +// LookupByCgroup looks up a pod by a cgroup fragment — pod UID variant +// (dashes or underscores) or container ID (full 64-char hex or 12-char prefix). +func (a *KubernetesAdapter) LookupByCgroup(fragment string) *PodInfo { + a.mu.RLock() + info := a.cgroupIndex[fragment] + a.mu.RUnlock() + return info } -// kubeletReadOnlyURL returns the Kubelet read-only API endpoint. -// Default port is 10255 unless overridden by env var. -func kubeletReadOnlyURL() string { - if url := os.Getenv("KERNO_KUBELET_URL"); url != "" { - return url +// LookupByPath resolves a cgroup path to a pod name and namespace. +// Implements collector.PodLookup. +func (a *KubernetesAdapter) LookupByPath(cgroupPath string) (pod, namespace string) { + uid := extractPodUID(cgroupPath) + if uid == "" { + return "", "" + } + info := a.LookupByUID(uid) + if info == nil { + info = a.LookupByCgroup(strings.ReplaceAll(uid, "-", "_")) + } + if info == nil { + return "", "" + } + return info.Name, info.Namespace +} + +// Enrich maps the cgroup path to K8s pod metadata. Stale index entries are +// preserved across API server disconnects so enrichment continues degraded. +func (a *KubernetesAdapter) Enrich(meta *EventMeta) { + meta.Hostname = a.hostname + meta.Node = a.nodeName + + if meta.PID > 0 && meta.CgroupPath == "" { + meta.CgroupPath = cgroupPathForPID(meta.PID) + } + if meta.CgroupPath == "" { + return + } + + uid := extractPodUID(meta.CgroupPath) + if uid == "" { + return + } + + info := a.LookupByUID(uid) + if info == nil { + info = a.LookupByCgroup(strings.ReplaceAll(uid, "-", "_")) + } + if info != nil { + meta.Pod = info.Name + meta.Namespace = info.Namespace + meta.Node = info.Node + meta.Deployment = info.Deployment } - return "http://localhost:10255" } // extractPodUID extracts a Kubernetes pod UID from a cgroup path. // -// cgroup v2 examples: -// -// /kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod.slice/cri-containerd-.scope -// /kubepods/burstable/pod/ -// -// cgroup v1 examples: -// -// /kubepods/burstable/pod/ -// /kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod.slice +// cgroup v1: /kubepods/burstable/pod/ +// cgroup v2 / systemd: /kubepods.slice/.../kubepods-burstable-pod.slice/... func extractPodUID(cgroupPath string) string { - // Look for "pod" followed by a UUID-like string. - // Common patterns: - // pod/ (cgroup v1) - // kubepods-*-pod.slice (cgroup v2 / systemd driver) - - // Try cgroup v1 style: ".../pod/..." - idx := strings.Index(cgroupPath, "/pod") - if idx >= 0 { - rest := cgroupPath[idx+4:] // skip "/pod" - // UID goes until next '/' or end. + // cgroup v1 style: ".../pod/..." + if idx := strings.Index(cgroupPath, "/pod"); idx >= 0 { + rest := cgroupPath[idx+4:] end := strings.IndexByte(rest, '/') if end < 0 { end = len(rest) @@ -298,17 +388,14 @@ func extractPodUID(cgroupPath string) string { } } - // Try cgroup v2 / systemd style: "...-pod.slice" - podIdx := strings.Index(cgroupPath, "-pod") - if podIdx >= 0 { - rest := cgroupPath[podIdx+4:] // skip "-pod" + // cgroup v2 / systemd style: "...-pod.slice" + if podIdx := strings.Index(cgroupPath, "-pod"); podIdx >= 0 { + rest := cgroupPath[podIdx+4:] end := strings.IndexByte(rest, '.') if end < 0 { end = len(rest) } - uid := rest[:end] - // Systemd escapes dashes: replace '_' with '-' in UID. - uid = strings.ReplaceAll(uid, "_", "-") + uid := strings.ReplaceAll(rest[:end], "_", "-") if isUIDLike(uid) { return uid } @@ -317,8 +404,7 @@ func extractPodUID(cgroupPath string) string { return "" } -// isUIDLike checks if a string looks like a Kubernetes UID (UUID format). -// We're lenient — just check length and allowed characters. +// isUIDLike checks if s looks like a Kubernetes UID (UUID-ish hex string). func isUIDLike(s string) bool { if len(s) < 32 { return false @@ -331,8 +417,8 @@ func isUIDLike(s string) bool { return true } -// extractDeploymentName extracts the deployment name from a ReplicaSet name. -// ReplicaSet names follow the pattern -. +// extractDeploymentName strips the trailing hash from a ReplicaSet name to +// recover the parent Deployment name. func extractDeploymentName(replicaSetName string) string { lastDash := strings.LastIndex(replicaSetName, "-") if lastDash <= 0 { diff --git a/internal/adapter/kubernetes_test.go b/internal/adapter/kubernetes_test.go new file mode 100644 index 0000000..cd9aacb --- /dev/null +++ b/internal/adapter/kubernetes_test.go @@ -0,0 +1,237 @@ +// Copyright 2026 Optiqor contributors +// SPDX-License-Identifier: Apache-2.0 + +package adapter + +import ( + "fmt" + "log/slog" + "sort" + "testing" + "time" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +const podCount = 1000 + +// makeFakePods builds n synthetic pods, each with one container and a unique +// container ID. UIDs and container IDs are deterministic so tests are stable. +func makeFakePods(n int) []*corev1.Pod { + pods := make([]*corev1.Pod, n) + for i := 0; i < n; i++ { + uid := fmt.Sprintf("%08x-0000-0000-0000-%012x", i, i) + // Index in leading bytes so every 12-char prefix is unique. + cid := fmt.Sprintf("%012x%052x", i, 0) + pods[i] = &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("pod-%d", i), + Namespace: fmt.Sprintf("ns-%d", i%10), + UID: types.UID(uid), + OwnerReferences: []metav1.OwnerReference{ + {Kind: "ReplicaSet", Name: fmt.Sprintf("deploy-%d-abc123def", i%50)}, + }, + }, + Spec: corev1.PodSpec{NodeName: "test-node"}, + Status: corev1.PodStatus{ + ContainerStatuses: []corev1.ContainerStatus{ + { + Name: "app", + ContainerID: "containerd://" + cid, + }, + }, + }, + } + } + return pods +} + +// warmAdapter builds an adapter and indexes all pods. +func warmAdapter(pods []*corev1.Pod) *KubernetesAdapter { + a := NewKubernetesAdapter(slog.Default()) + for _, pod := range pods { + a.addPod(pod) + } + return a +} + +// TestKubernetesAdapter_CorrectnessUID verifies every pod is resolvable by UID. +func TestKubernetesAdapter_CorrectnessUID(t *testing.T) { + pods := makeFakePods(podCount) + a := warmAdapter(pods) + + for _, pod := range pods { + uid := string(pod.UID) + info := a.LookupByUID(uid) + if info == nil { + t.Errorf("LookupByUID(%q) = nil", uid) + continue + } + if info.Name != pod.Name { + t.Errorf("LookupByUID(%q).Name = %q, want %q", uid, info.Name, pod.Name) + } + } +} + +// TestKubernetesAdapter_CorrectnessCgroup verifies every pod is resolvable by +// its 12-character container ID prefix (the fragment embedded in cgroup paths). +func TestKubernetesAdapter_CorrectnessCgroup(t *testing.T) { + pods := makeFakePods(podCount) + a := warmAdapter(pods) + + for _, pod := range pods { + cid := pod.Status.ContainerStatuses[0].ContainerID[len("containerd://"):] + prefix := cid[:12] + info := a.LookupByCgroup(prefix) + if info == nil { + t.Errorf("LookupByCgroup(%q) = nil for pod %s", prefix, pod.Name) + continue + } + if info.Name != pod.Name { + t.Errorf("LookupByCgroup(%q).Name = %q, want %q", prefix, info.Name, pod.Name) + } + } +} + +// TestKubernetesAdapter_CorrectnessRemove verifies that deleted pods are no +// longer resolvable. +func TestKubernetesAdapter_CorrectnessRemove(t *testing.T) { + pods := makeFakePods(10) + a := warmAdapter(pods) + + target := pods[3] + a.removePod(target) + + if info := a.LookupByUID(string(target.UID)); info != nil { + t.Errorf("LookupByUID after removePod: expected nil, got %+v", info) + } + cid := target.Status.ContainerStatuses[0].ContainerID[len("containerd://"):] + if info := a.LookupByCgroup(cid[:12]); info != nil { + t.Errorf("LookupByCgroup after removePod: expected nil, got %+v", info) + } + + // Other pods must still be resolvable. + for i, pod := range pods { + if i == 3 { + continue + } + if info := a.LookupByUID(string(pod.UID)); info == nil { + t.Errorf("pod %d unexpectedly missing after unrelated remove", i) + } + } +} + +// TestKubernetesAdapter_LookupByPath exercises the PodLookup interface using +// cgroup v1 and cgroup v2 / systemd path formats. +func TestKubernetesAdapter_LookupByPath(t *testing.T) { + pods := makeFakePods(5) + a := warmAdapter(pods) + + pod := pods[0] + uid := string(pod.UID) + uidUnderscore := replaceAll(uid, "-", "_") + + tests := []struct { + name string + path string + wantP string + }{ + { + "cgroup v1", + "/kubepods/burstable/pod" + uid + "/abc123", + pod.Name, + }, + { + "cgroup v2 systemd", + "/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod" + uidUnderscore + ".slice/cri-containerd-xyz.scope", + pod.Name, + }, + { + "no match", + "/system.slice/nginx.service", + "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotPod, _ := a.LookupByPath(tt.path) + if gotPod != tt.wantP { + t.Errorf("LookupByPath(%q) pod = %q, want %q", tt.path, gotPod, tt.wantP) + } + }) + } +} + +// replaceAll is strings.ReplaceAll kept local to avoid importing strings just +// for the test helper. +func replaceAll(s, from, to string) string { + out := make([]byte, 0, len(s)) + for i := 0; i < len(s); { + if i+len(from) <= len(s) && s[i:i+len(from)] == from { + out = append(out, to...) + i += len(from) + } else { + out = append(out, s[i]) + i++ + } + } + return string(out) +} + +// TestKubernetesAdapter_LookupLatency asserts p99 < 10µs for both lookup types +// after cache warmup with podCount pods. Each lookup is a single RLock + map +// read + RUnlock. +func TestKubernetesAdapter_LookupLatency(t *testing.T) { + pods := makeFakePods(podCount) + a := warmAdapter(pods) + + uids := make([]string, podCount) + prefixes := make([]string, podCount) + for i, pod := range pods { + uids[i] = string(pod.UID) + cid := pod.Status.ContainerStatuses[0].ContainerID[len("containerd://"):] + prefixes[i] = cid[:12] + } + + const warmupRuns = 5 + const measuredRuns = podCount + + // UID lookup latency. + uidLatencies := make([]time.Duration, measuredRuns) + for w := 0; w < warmupRuns; w++ { + for _, uid := range uids { + a.LookupByUID(uid) + } + } + for i, uid := range uids { + start := time.Now() + a.LookupByUID(uid) + uidLatencies[i] = time.Since(start) + } + sort.Slice(uidLatencies, func(i, j int) bool { return uidLatencies[i] < uidLatencies[j] }) + p99uid := uidLatencies[int(float64(measuredRuns)*0.99)] + if p99uid > 10*time.Microsecond { + t.Errorf("LookupByUID p99 = %v, want < 10µs", p99uid) + } + + // Cgroup lookup latency. + cgroupLatencies := make([]time.Duration, measuredRuns) + for w := 0; w < warmupRuns; w++ { + for _, pfx := range prefixes { + a.LookupByCgroup(pfx) + } + } + for i, pfx := range prefixes { + start := time.Now() + a.LookupByCgroup(pfx) + cgroupLatencies[i] = time.Since(start) + } + sort.Slice(cgroupLatencies, func(i, j int) bool { return cgroupLatencies[i] < cgroupLatencies[j] }) + p99cg := cgroupLatencies[int(float64(measuredRuns)*0.99)] + if p99cg > 10*time.Microsecond { + t.Errorf("LookupByCgroup p99 = %v, want < 10µs", p99cg) + } +}