From eb8925802bade1cd8c4e689dbcf2ee966c608789 Mon Sep 17 00:00:00 2001 From: Shingo Omura Date: Thu, 19 Jun 2025 22:15:04 +0900 Subject: [PATCH 01/10] Added Cluster Inventory API provider Signed-off-by: Shingo Omura --- providers/cluster-inventory-api/go.mod | 64 +++++ providers/cluster-inventory-api/go.sum | 191 +++++++++++++ providers/cluster-inventory-api/provider.go | 292 ++++++++++++++++++++ 3 files changed, 547 insertions(+) create mode 100644 providers/cluster-inventory-api/go.mod create mode 100644 providers/cluster-inventory-api/go.sum create mode 100644 providers/cluster-inventory-api/provider.go diff --git a/providers/cluster-inventory-api/go.mod b/providers/cluster-inventory-api/go.mod new file mode 100644 index 0000000..395a5d8 --- /dev/null +++ b/providers/cluster-inventory-api/go.mod @@ -0,0 +1,64 @@ +module sigs.k8s.io/multicluster-runtime/providers/cluster-inventory-api + +go 1.24.2 + +require ( + github.com/go-logr/logr v1.4.2 + k8s.io/api v0.33.0 + k8s.io/apimachinery v0.33.0 + k8s.io/client-go v0.33.0 + sigs.k8s.io/cluster-inventory-api v0.0.0-20250318031555-c7c0594aa53b + sigs.k8s.io/controller-runtime v0.21.0 + sigs.k8s.io/multicluster-runtime v0.21.0-alpha.8 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-openapi/jsonpointer v0.21.1 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/gnostic-models v0.6.9 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.9.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.22.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/x448/float16 v0.8.4 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/oauth2 v0.28.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/term v0.30.0 // indirect + golang.org/x/text v0.23.0 // indirect + golang.org/x/time v0.11.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/protobuf v1.36.5 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apiextensions-apiserver v0.33.0 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect + k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/providers/cluster-inventory-api/go.sum b/providers/cluster-inventory-api/go.sum new file mode 100644 index 0000000..b14d621 --- /dev/null +++ b/providers/cluster-inventory-api/go.sum @@ -0,0 +1,191 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +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.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= +github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= +github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= +github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= +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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= +github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= +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-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +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/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/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.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +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.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +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 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/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 v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= +github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= +github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +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.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +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.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= +golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +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.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +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.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +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.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +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.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +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= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +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/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +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.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU= +k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM= +k8s.io/apiextensions-apiserver v0.33.0 h1:d2qpYL7Mngbsc1taA4IjJPRJ9ilnsXIrndH+r9IimOs= +k8s.io/apiextensions-apiserver v0.33.0/go.mod h1:VeJ8u9dEEN+tbETo+lFkwaaZPg6uFKLGj5vyNEwwSzc= +k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ= +k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/client-go v0.33.0 h1:UASR0sAYVUzs2kYuKn/ZakZlcs2bEHaizrrHUZg0G98= +k8s.io/client-go v0.33.0/go.mod h1:kGkd+l/gNGg8GYWAPr0xF1rRKvVWvzh9vmZAMXtaKOg= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= +k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0= +k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/cluster-inventory-api v0.0.0-20250318031555-c7c0594aa53b h1:dxgZ2Icq72axrMMtZ4NbfDRJtW40GhJ0VirvbksmYeg= +sigs.k8s.io/cluster-inventory-api v0.0.0-20250318031555-c7c0594aa53b/go.mod h1:oAC/t/ChRw8Q8mQGq6Dqurf85SxRrhX1WDseqgwlnTo= +sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8= +sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/multicluster-runtime v0.21.0-alpha.8 h1:Pq69tTKfN8ADw8m8A3wUtP8wJ9SPQbbOsgapm3BZEPw= +sigs.k8s.io/multicluster-runtime v0.21.0-alpha.8/go.mod h1:CpBzLMLQKdm+UCchd2FiGPiDdCxM5dgCCPKuaQ6Fsv0= +sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/providers/cluster-inventory-api/provider.go b/providers/cluster-inventory-api/provider.go new file mode 100644 index 0000000..e02bed8 --- /dev/null +++ b/providers/cluster-inventory-api/provider.go @@ -0,0 +1,292 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package clusterinventoryapi + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/go-logr/logr" + + clusterinventoryv1alpha1 "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/cluster" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" + "sigs.k8s.io/multicluster-runtime/pkg/multicluster" +) + +var _ multicluster.Provider = &Provider{} + +var ( + // GetKubeConfigFromSecret is a function that fetches the kubeconfig for a ClusterProfile from Secret + // It supposes that the Secret is managed by following "Push Model via Credentials in Secret" in "KEP-4322: ClusterProfile API" + // ref: https://github.com/kubernetes/enhancements/blob/master/keps/sig-multicluster/4322-cluster-inventory/README.md#push-model-via-credentials-in-secret-not-recommended + GetKubeConfigFromSecret = func(ctx context.Context, cli client.Client, consumerName string, clp *clusterinventoryv1alpha1.ClusterProfile) (*rest.Config, error) { + secrets := corev1.SecretList{} + if err := cli.List(ctx, &secrets, client.InNamespace(clp.Namespace), client.MatchingLabels{ + "x-k8s.io/cluster-inventory-consumer": consumerName, + "x-k8s.io/cluster-profile": clp.Name, + }); err != nil { + return nil, fmt.Errorf("failed to list secrets: %w", err) + } + + if len(secrets.Items) == 0 { + return nil, fmt.Errorf("no secrets found") + } + + if len(secrets.Items) > 1 { + return nil, fmt.Errorf("multiple secrets found, expected one, got %d", len(secrets.Items)) + } + + secret := secrets.Items[0] + + data, ok := secret.Data["Config"] + if !ok { + return nil, fmt.Errorf("secret %s/%s does not contain Config data", secret.Namespace, secret.Name) + } + return clientcmd.RESTConfigFromKubeConfig(data) + } +) + +// Options are the options for the Cluster-API cluster Provider. +type Options struct { + // ConsumerName is the name of the consumer that will use the cluster inventory API. + ConsumerName string + + // ClusterOptions are the options passed to the cluster constructor. + ClusterOptions []cluster.Option + + // GetKubeConfig is a function that returns the kubeconfig secret for a cluster profile. + GetKubeConfig func(ctx context.Context, cli client.Client, consumerName string, clp *clusterinventoryv1alpha1.ClusterProfile) (*rest.Config, error) + + // NewCluster is a function that creates a new cluster from a rest.Config. + // The cluster will be started by the provider. + NewCluster func(ctx context.Context, clp *clusterinventoryv1alpha1.ClusterProfile, cfg *rest.Config, opts ...cluster.Option) (cluster.Cluster, error) +} + +func setDefaults(opts *Options, cli client.Client) { + if opts.GetKubeConfig == nil { + opts.GetKubeConfig = GetKubeConfigFromSecret + } + if opts.NewCluster == nil { + opts.NewCluster = func(ctx context.Context, clp *clusterinventoryv1alpha1.ClusterProfile, cfg *rest.Config, opts ...cluster.Option) (cluster.Cluster, error) { + return cluster.New(cfg, opts...) + } + } +} + +// New creates a new Cluster Inventory API cluster Provider. +func New(localMgr manager.Manager, opts Options) (*Provider, error) { + p := &Provider{ + opts: opts, + log: log.Log.WithName("cluster-inventory-api-cluster-provider"), + client: localMgr.GetClient(), + clusters: map[string]cluster.Cluster{}, + cancelFns: map[string]context.CancelFunc{}, + } + + setDefaults(&p.opts, p.client) + + if err := builder.ControllerManagedBy(localMgr). + For(&clusterinventoryv1alpha1.ClusterProfile{}). + WithOptions(controller.Options{MaxConcurrentReconciles: 1}). // no prallelism. + Complete(p); err != nil { + return nil, fmt.Errorf("failed to create controller: %w", err) + } + + return p, nil +} + +type index struct { + object client.Object + field string + extractValue client.IndexerFunc +} + +// Provider is a cluster Provider that works with Cluster Inventory API. +type Provider struct { + opts Options + log logr.Logger + client client.Client + + lock sync.RWMutex + mcMgr mcmanager.Manager + clusters map[string]cluster.Cluster + cancelFns map[string]context.CancelFunc + indexers []index +} + +// Get returns the cluster with the given name, if it is known. +func (p *Provider) Get(_ context.Context, clusterName string) (cluster.Cluster, error) { + p.lock.RLock() + defer p.lock.RUnlock() + if cl, ok := p.clusters[clusterName]; ok { + return cl, nil + } + + return nil, multicluster.ErrClusterNotFound +} + +// Run starts the provider and blocks. +func (p *Provider) Run(ctx context.Context, mgr mcmanager.Manager) error { + p.log.Info("Starting Cluster Inventory API cluster provider") + + p.lock.Lock() + p.mcMgr = mgr + p.lock.Unlock() + + <-ctx.Done() + + return ctx.Err() +} + +func (p *Provider) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { + key := req.NamespacedName.String() + + log := p.log.WithValues("clusterprofile", key) + log.Info("Reconciling ClusterProfile") + + // get the cluster + clp := &clusterinventoryv1alpha1.ClusterProfile{} + if err := p.client.Get(ctx, req.NamespacedName, clp); err != nil { + if apierrors.IsNotFound(err) { + log.Error(err, "failed to get cluster profile") + + p.lock.Lock() + defer p.lock.Unlock() + + delete(p.clusters, key) + if cancel, ok := p.cancelFns[key]; ok { + cancel() + } + + return reconcile.Result{}, nil + } + + return reconcile.Result{}, fmt.Errorf("failed to get ClusterProfile %s: %w", key, err) + } + log.V(3).Info("Found ClusterProfile") + + p.lock.Lock() + defer p.lock.Unlock() + + // provider already started? + if p.mcMgr == nil { + log.V(3).Info("Provider not started yet, requeuing") + return reconcile.Result{RequeueAfter: time.Second * 2}, nil + } + + // already engaged? + if _, ok := p.clusters[key]; ok { + log.Info("ClusterProfile already engaged") + return reconcile.Result{}, nil + } + + // ready? + controlPlaneHealthyCondition := meta.FindStatusCondition(clp.Status.Conditions, clusterinventoryv1alpha1.ClusterConditionControlPlaneHealthy) + if controlPlaneHealthyCondition == nil || controlPlaneHealthyCondition.Status != metav1.ConditionTrue { + log.Info("ClusterProfile is not healthy yet, requeuing") + return reconcile.Result{RequeueAfter: time.Second * 10}, nil + } + + // get kubeconfig + cfg, err := p.opts.GetKubeConfig(ctx, p.client, p.opts.ConsumerName, clp) + if err != nil { + log.Error(err, "Failed to get kubeconfig for ClusterProfile") + return reconcile.Result{}, fmt.Errorf("failed to get kubeconfig for ClusterProfile=%s: %w", key, err) + } + + // create cluster. + cl, err := p.opts.NewCluster(ctx, clp, cfg, p.opts.ClusterOptions...) + if err != nil { + log.Error(err, "Failed to create cluster for ClusterProfile") + return reconcile.Result{}, fmt.Errorf("failed to create cluster for ClusterProfile=%s: %w", key, err) + } + for _, idx := range p.indexers { + if err := cl.GetCache().IndexField(ctx, idx.object, idx.field, idx.extractValue); err != nil { + return reconcile.Result{}, fmt.Errorf("failed to index field %q for ClusterProfile=%s: %w", idx.field, key, err) + } + } + clusterCtx, cancel := context.WithCancel(ctx) + go func() { + if err := cl.Start(clusterCtx); err != nil { + log.Error(err, "failed to start cluster for ClusterProfile") + return + } + }() + if !cl.GetCache().WaitForCacheSync(ctx) { + cancel() + log.Error(nil, "failed to sync cache for ClusterProfile") + return reconcile.Result{}, fmt.Errorf("failed to sync cache for ClusterProfile=%s", key) + } + + // remember. + p.clusters[key] = cl + p.cancelFns[key] = cancel + + log.Info("Added new cluster for ClusterProfile") + + // engage manager. + if err := p.mcMgr.Engage(clusterCtx, key, cl); err != nil { + log.Error(err, "failed to engage manager for ClusterProfile") + delete(p.clusters, key) + delete(p.cancelFns, key) + return reconcile.Result{}, err + } + + log.Info("Cluster engaged manager for ClusterProfile") + return reconcile.Result{}, nil +} + +// IndexField indexes a field on all clusters, existing and future. +func (p *Provider) IndexField(ctx context.Context, obj client.Object, field string, extractValue client.IndexerFunc) error { + p.lock.Lock() + defer p.lock.Unlock() + + // save for future clusters. + p.indexers = append(p.indexers, index{ + object: obj, + field: field, + extractValue: extractValue, + }) + + // apply to existing clusters. + for clusterProfileName, cl := range p.clusters { + if err := cl.GetCache().IndexField(ctx, obj, field, extractValue); err != nil { + p.log.Error(err, "Failed to index field on existing cluster", "field", field, "clusterprofile", clusterProfileName) + return fmt.Errorf("failed to index field %q on ClusterProfile %q: %w", field, clusterProfileName, err) + } + } + + return nil +} From 11aa126c8be4d43879b7420763c42fdc2fa0245e Mon Sep 17 00:00:00 2001 From: Shingo Omura Date: Thu, 19 Jun 2025 22:15:46 +0900 Subject: [PATCH 02/10] Added an example for the Cluster Inventory API provider Signed-off-by: Shingo Omura --- examples/cluster-inventory-api/go.mod | 73 ++++++++++ examples/cluster-inventory-api/go.sum | 189 +++++++++++++++++++++++++ examples/cluster-inventory-api/main.go | 142 +++++++++++++++++++ hack/check-everything.sh | 1 + 4 files changed, 405 insertions(+) create mode 100644 examples/cluster-inventory-api/go.mod create mode 100644 examples/cluster-inventory-api/go.sum create mode 100644 examples/cluster-inventory-api/main.go diff --git a/examples/cluster-inventory-api/go.mod b/examples/cluster-inventory-api/go.mod new file mode 100644 index 0000000..e5e7e04 --- /dev/null +++ b/examples/cluster-inventory-api/go.mod @@ -0,0 +1,73 @@ +module sigs.k8s.io/multicluster-runtime/examples/cluster-inventory-api + +go 1.24.2 + +replace ( + sigs.k8s.io/multicluster-runtime => ../.. + sigs.k8s.io/multicluster-runtime/providers/cluster-inventory-api => ../../providers/cluster-inventory-api +) + +require ( + golang.org/x/sync v0.15.0 + k8s.io/api v0.33.1 + k8s.io/apimachinery v0.33.1 + k8s.io/client-go v0.33.1 + sigs.k8s.io/cluster-inventory-api v0.0.0-20250318031555-c7c0594aa53b + sigs.k8s.io/controller-runtime v0.21.0 + sigs.k8s.io/multicluster-runtime v0.21.0-alpha.8 + sigs.k8s.io/multicluster-runtime/providers/cluster-inventory-api v0.0.0-00010101000000-000000000000 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/zapr v1.3.0 // indirect + github.com/go-openapi/jsonpointer v0.21.1 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/gnostic-models v0.6.9 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.9.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.22.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/oauth2 v0.28.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/term v0.30.0 // indirect + golang.org/x/text v0.23.0 // indirect + golang.org/x/time v0.11.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/protobuf v1.36.5 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apiextensions-apiserver v0.33.0 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect + k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/examples/cluster-inventory-api/go.sum b/examples/cluster-inventory-api/go.sum new file mode 100644 index 0000000..ae46415 --- /dev/null +++ b/examples/cluster-inventory-api/go.sum @@ -0,0 +1,189 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +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.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= +github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= +github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= +github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= +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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= +github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= +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-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +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/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/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.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +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.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +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 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/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 v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= +github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= +github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +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.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +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.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= +golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +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.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +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.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +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.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +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.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +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= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +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/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +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.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw= +k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw= +k8s.io/apiextensions-apiserver v0.33.0 h1:d2qpYL7Mngbsc1taA4IjJPRJ9ilnsXIrndH+r9IimOs= +k8s.io/apiextensions-apiserver v0.33.0/go.mod h1:VeJ8u9dEEN+tbETo+lFkwaaZPg6uFKLGj5vyNEwwSzc= +k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= +k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4= +k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= +k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0= +k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/cluster-inventory-api v0.0.0-20250318031555-c7c0594aa53b h1:dxgZ2Icq72axrMMtZ4NbfDRJtW40GhJ0VirvbksmYeg= +sigs.k8s.io/cluster-inventory-api v0.0.0-20250318031555-c7c0594aa53b/go.mod h1:oAC/t/ChRw8Q8mQGq6Dqurf85SxRrhX1WDseqgwlnTo= +sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8= +sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/examples/cluster-inventory-api/main.go b/examples/cluster-inventory-api/main.go new file mode 100644 index 0000000..df6f0c7 --- /dev/null +++ b/examples/cluster-inventory-api/main.go @@ -0,0 +1,142 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "context" + "errors" + "os" + + "golang.org/x/sync/errgroup" + clusterinventoryv1alpha1 "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/client-go/kubernetes/scheme" + + ctrl "sigs.k8s.io/controller-runtime" + ctrllog "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/manager/signals" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + mcbuilder "sigs.k8s.io/multicluster-runtime/pkg/builder" + mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" + mcreconcile "sigs.k8s.io/multicluster-runtime/pkg/reconcile" + clusterinventoryapi "sigs.k8s.io/multicluster-runtime/providers/cluster-inventory-api" +) + +func init() { + runtime.Must(clusterinventoryv1alpha1.AddToScheme(scheme.Scheme)) +} + +func main() { + ctrllog.SetLogger(zap.New(zap.UseDevMode(true))) + entryLog := ctrllog.Log.WithName("entrypoint") + ctx := signals.SetupSignalHandler() + + // Start local manager to read the Cluster-API objects. + cfg, err := ctrl.GetConfig() + if err != nil { + entryLog.Error(err, "unable to get kubeconfig") + os.Exit(1) + } + localMgr, err := manager.New(cfg, manager.Options{}) + if err != nil { + entryLog.Error(err, "unable to set up overall controller manager") + os.Exit(1) + } + + // Create the provider against the local manager. + provider, err := clusterinventoryapi.New(localMgr, clusterinventoryapi.Options{ + ConsumerName: "cluster-inventory-api-consumer", + }) + if err != nil { + entryLog.Error(err, "unable to create provider") + os.Exit(1) + } + + // Create a multi-cluster manager attached to the provider. + entryLog.Info("Setting up local manager") + mcMgr, err := mcmanager.New(cfg, provider, manager.Options{ + LeaderElection: false, + Metrics: metricsserver.Options{ + BindAddress: "0", // only one can listen + }, + }) + if err != nil { + entryLog.Error(err, "unable to set up overall controller manager") + os.Exit(1) + } + + // Create a configmap controller in the multi-cluster manager. + if err := mcbuilder.ControllerManagedBy(mcMgr). + Named("multicluster-configmaps"). + For(&corev1.ConfigMap{}). + Complete(mcreconcile.Func( + func(ctx context.Context, req mcreconcile.Request) (ctrl.Result, error) { + log := ctrllog.FromContext(ctx).WithValues("cluster", req.ClusterName) + log.Info("Reconciling ConfigMap") + + cl, err := mcMgr.GetCluster(ctx, req.ClusterName) + if err != nil { + return reconcile.Result{}, err + } + + cm := &corev1.ConfigMap{} + if err := cl.GetClient().Get(ctx, req.Request.NamespacedName, cm); err != nil { + if apierrors.IsNotFound(err) { + return reconcile.Result{}, nil + } + return reconcile.Result{}, err + } + + log.Info("ConfigMap in cluster", "configmap", cm.Namespace+"/"+cm.Name, "cluster", req.ClusterName) + + return ctrl.Result{}, nil + }, + )); err != nil { + entryLog.Error(err, "failed to build controller") + os.Exit(1) + } + + // Starting everything. + g, ctx := errgroup.WithContext(ctx) + g.Go(func() error { + return ignoreCanceled(localMgr.Start(ctx)) + }) + g.Go(func() error { + return ignoreCanceled(provider.Run(ctx, mcMgr)) + }) + g.Go(func() error { + return ignoreCanceled(mcMgr.Start(ctx)) + }) + if err := g.Wait(); err != nil { + entryLog.Error(err, "unable to start") + os.Exit(1) + } +} + +func ignoreCanceled(err error) error { + if errors.Is(err, context.Canceled) { + return nil + } + return err +} diff --git a/hack/check-everything.sh b/hack/check-everything.sh index 4db87a7..ddf2bb4 100755 --- a/hack/check-everything.sh +++ b/hack/check-everything.sh @@ -47,6 +47,7 @@ header_text "confirming examples compile (via go install)" pushd examples/kind; go install ${MOD_OPT} .; popd pushd examples/namespace; go install ${MOD_OPT} .; popd pushd examples/cluster-api; go install ${MOD_OPT} .; popd +pushd examples/cluster-inventory-api; go install ${MOD_OPT} .; popd echo "passed" exit 0 From b4dc008275f2fcc17e1e89f5c5adacfd36fb9689 Mon Sep 17 00:00:00 2001 From: Shingo Omura Date: Fri, 20 Jun 2025 14:33:20 +0900 Subject: [PATCH 03/10] Update examples/cluster-inventory-api/main.go Signed-off-by: Shingo Omura --- examples/cluster-inventory-api/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/cluster-inventory-api/main.go b/examples/cluster-inventory-api/main.go index df6f0c7..1f46a8f 100644 --- a/examples/cluster-inventory-api/main.go +++ b/examples/cluster-inventory-api/main.go @@ -52,7 +52,7 @@ func main() { entryLog := ctrllog.Log.WithName("entrypoint") ctx := signals.SetupSignalHandler() - // Start local manager to read the Cluster-API objects. + // Start local manager to read the Cluster Inventory API objects. cfg, err := ctrl.GetConfig() if err != nil { entryLog.Error(err, "unable to get kubeconfig") From cefe93a3bbe012ad62ab0f332399a264844cfeb3 Mon Sep 17 00:00:00 2001 From: Shingo Omura Date: Fri, 20 Jun 2025 17:21:02 +0900 Subject: [PATCH 04/10] Support re-engaging a cluster in multicluster manager when the kubeconfig is changed. Signed-off-by: Shingo Omura --- providers/cluster-inventory-api/provider.go | 196 ++++++++++++++------ 1 file changed, 141 insertions(+), 55 deletions(-) diff --git a/providers/cluster-inventory-api/provider.go b/providers/cluster-inventory-api/provider.go index e02bed8..077158e 100644 --- a/providers/cluster-inventory-api/provider.go +++ b/providers/cluster-inventory-api/provider.go @@ -19,6 +19,7 @@ package clusterinventoryapi import ( "context" "fmt" + "reflect" "sync" "time" @@ -30,6 +31,7 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" @@ -37,8 +39,10 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/cluster" "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" @@ -47,15 +51,66 @@ import ( var _ multicluster.Provider = &Provider{} -var ( - // GetKubeConfigFromSecret is a function that fetches the kubeconfig for a ClusterProfile from Secret - // It supposes that the Secret is managed by following "Push Model via Credentials in Secret" in "KEP-4322: ClusterProfile API" - // ref: https://github.com/kubernetes/enhancements/blob/master/keps/sig-multicluster/4322-cluster-inventory/README.md#push-model-via-credentials-in-secret-not-recommended - GetKubeConfigFromSecret = func(ctx context.Context, cli client.Client, consumerName string, clp *clusterinventoryv1alpha1.ClusterProfile) (*rest.Config, error) { +const ( + labelKeyClusterInventoryConsumer = "x-k8s.io/cluster-inventory-consumer" + labelKeyClusterProfile = "x-k8s.io/cluster-profile" +) + +// Options are the options for the Cluster-API cluster Provider. +type Options struct { + // ConsumerName is the name of the consumer that will use the cluster inventory API. + ConsumerName string + + // ClusterOptions are the options passed to the cluster constructor. + ClusterOptions []cluster.Option + + // GetKubeConfig is a function that returns the kubeconfig secret for a cluster profile. + GetKubeConfig func(ctx context.Context, cli client.Client, clp *clusterinventoryv1alpha1.ClusterProfile) (*rest.Config, error) + + // NewCluster is a function that creates a new cluster from a rest.Config. + // The cluster will be started by the provider. + NewCluster func(ctx context.Context, clp *clusterinventoryv1alpha1.ClusterProfile, cfg *rest.Config, opts ...cluster.Option) (cluster.Cluster, error) + + // CustomWatches can add custom watches to the provider controller + CustomWatches []CustomWatch +} + +// CustomWatch specifies a custom watch spec that can be added to the provider controller. +type CustomWatch struct { + Object client.Object + EventHandler handler.TypedEventHandler[client.Object, reconcile.Request] + Opts []builder.WatchesOption +} + +type index struct { + object client.Object + field string + extractValue client.IndexerFunc +} + +// Provider is a cluster Provider that works with Cluster Inventory API. +type Provider struct { + opts Options + log logr.Logger + client client.Client + + lock sync.RWMutex + mcMgr mcmanager.Manager + clusters map[string]cluster.Cluster + cancelFns map[string]context.CancelFunc + kubeconfig map[string]*rest.Config + indexers []index +} + +// GetKubeConfigFromSecret returns a function that fetches the kubeconfig for a specified consumer for ClusterProfile from Secret +// It supposes that the Secrets for ClusterProfiles are managed by following "Push Model via Credentials in Secret" in "KEP-4322: ClusterProfile API" +// ref: https://github.com/kubernetes/enhancements/blob/master/keps/sig-multicluster/4322-cluster-inventory/README.md#push-model-via-credentials-in-secret-not-recommended +func GetKubeConfigFromSecret(consumerName string) func(ctx context.Context, cli client.Client, clp *clusterinventoryv1alpha1.ClusterProfile) (*rest.Config, error) { + return func(ctx context.Context, cli client.Client, clp *clusterinventoryv1alpha1.ClusterProfile) (*rest.Config, error) { secrets := corev1.SecretList{} if err := cli.List(ctx, &secrets, client.InNamespace(clp.Namespace), client.MatchingLabels{ - "x-k8s.io/cluster-inventory-consumer": consumerName, - "x-k8s.io/cluster-profile": clp.Name, + labelKeyClusterInventoryConsumer: consumerName, + labelKeyClusterProfile: clp.Name, }); err != nil { return nil, fmt.Errorf("failed to list secrets: %w", err) } @@ -76,27 +131,50 @@ var ( } return clientcmd.RESTConfigFromKubeConfig(data) } -) - -// Options are the options for the Cluster-API cluster Provider. -type Options struct { - // ConsumerName is the name of the consumer that will use the cluster inventory API. - ConsumerName string +} - // ClusterOptions are the options passed to the cluster constructor. - ClusterOptions []cluster.Option +// WatchKubeConfigSecret returns a CustomWatch that watches for kubeconfig secrets for specified consumer of ClusterProfile +// It supposes that the Secrets for ClusterProfiles are managed by following "Push Model via Credentials in Secret" in "KEP-4322: ClusterProfile API" +// ref: https://github.com/kubernetes/enhancements/blob/master/keps/sig-multicluster/4322-cluster-inventory/README.md#push-model-via-credentials-in-secret-not-recommended +func WatchKubeConfigSecret(consumerName string) CustomWatch { + return CustomWatch{ + Object: &corev1.Secret{}, + EventHandler: handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request { + secret, ok := obj.(*corev1.Secret) + if !ok { + return nil + } - // GetKubeConfig is a function that returns the kubeconfig secret for a cluster profile. - GetKubeConfig func(ctx context.Context, cli client.Client, consumerName string, clp *clusterinventoryv1alpha1.ClusterProfile) (*rest.Config, error) + if secret.GetLabels() == nil || + secret.GetLabels()[labelKeyClusterInventoryConsumer] != consumerName || + secret.GetLabels()[labelKeyClusterProfile] == "" { + return nil + } - // NewCluster is a function that creates a new cluster from a rest.Config. - // The cluster will be started by the provider. - NewCluster func(ctx context.Context, clp *clusterinventoryv1alpha1.ClusterProfile, cfg *rest.Config, opts ...cluster.Option) (cluster.Cluster, error) + return []reconcile.Request{{ + NamespacedName: types.NamespacedName{ + Namespace: secret.GetNamespace(), + Name: secret.GetLabels()[labelKeyClusterProfile], + }, + }} + }), + Opts: []builder.WatchesOption{ + builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool { + secret, ok := object.(*corev1.Secret) + if !ok { + return false + } + return secret.GetLabels()[labelKeyClusterInventoryConsumer] == consumerName && + secret.GetLabels()[labelKeyClusterProfile] != "" + })), + }, + } } func setDefaults(opts *Options, cli client.Client) { if opts.GetKubeConfig == nil { - opts.GetKubeConfig = GetKubeConfigFromSecret + opts.GetKubeConfig = GetKubeConfigFromSecret(opts.ConsumerName) + opts.CustomWatches = append(opts.CustomWatches, WatchKubeConfigSecret(opts.ConsumerName)) } if opts.NewCluster == nil { opts.NewCluster = func(ctx context.Context, clp *clusterinventoryv1alpha1.ClusterProfile, cfg *rest.Config, opts ...cluster.Option) (cluster.Cluster, error) { @@ -108,44 +186,38 @@ func setDefaults(opts *Options, cli client.Client) { // New creates a new Cluster Inventory API cluster Provider. func New(localMgr manager.Manager, opts Options) (*Provider, error) { p := &Provider{ - opts: opts, - log: log.Log.WithName("cluster-inventory-api-cluster-provider"), - client: localMgr.GetClient(), - clusters: map[string]cluster.Cluster{}, - cancelFns: map[string]context.CancelFunc{}, + opts: opts, + log: log.Log.WithName("cluster-inventory-api-cluster-provider"), + client: localMgr.GetClient(), + clusters: map[string]cluster.Cluster{}, + cancelFns: map[string]context.CancelFunc{}, + kubeconfig: map[string]*rest.Config{}, } setDefaults(&p.opts, p.client) - if err := builder.ControllerManagedBy(localMgr). + // Create a controller builder + controllerBuilder := builder.ControllerManagedBy(localMgr). For(&clusterinventoryv1alpha1.ClusterProfile{}). - WithOptions(controller.Options{MaxConcurrentReconciles: 1}). // no prallelism. - Complete(p); err != nil { + WithOptions(controller.Options{MaxConcurrentReconciles: 1}) // no parallelism. + + // Apply any custom watches provided by the user + for _, customWatch := range p.opts.CustomWatches { + controllerBuilder.Watches( + customWatch.Object, + customWatch.EventHandler, + customWatch.Opts..., + ) + } + + // Complete the controller setup + if err := controllerBuilder.Complete(p); err != nil { return nil, fmt.Errorf("failed to create controller: %w", err) } return p, nil } -type index struct { - object client.Object - field string - extractValue client.IndexerFunc -} - -// Provider is a cluster Provider that works with Cluster Inventory API. -type Provider struct { - opts Options - log logr.Logger - client client.Client - - lock sync.RWMutex - mcMgr mcmanager.Manager - clusters map[string]cluster.Cluster - cancelFns map[string]context.CancelFunc - indexers []index -} - // Get returns the cluster with the given name, if it is known. func (p *Provider) Get(_ context.Context, clusterName string) (cluster.Cluster, error) { p.lock.RLock() @@ -206,12 +278,6 @@ func (p *Provider) Reconcile(ctx context.Context, req reconcile.Request) (reconc return reconcile.Result{RequeueAfter: time.Second * 2}, nil } - // already engaged? - if _, ok := p.clusters[key]; ok { - log.Info("ClusterProfile already engaged") - return reconcile.Result{}, nil - } - // ready? controlPlaneHealthyCondition := meta.FindStatusCondition(clp.Status.Conditions, clusterinventoryv1alpha1.ClusterConditionControlPlaneHealthy) if controlPlaneHealthyCondition == nil || controlPlaneHealthyCondition.Status != metav1.ConditionTrue { @@ -220,12 +286,30 @@ func (p *Provider) Reconcile(ctx context.Context, req reconcile.Request) (reconc } // get kubeconfig - cfg, err := p.opts.GetKubeConfig(ctx, p.client, p.opts.ConsumerName, clp) + cfg, err := p.opts.GetKubeConfig(ctx, p.client, clp) if err != nil { log.Error(err, "Failed to get kubeconfig for ClusterProfile") return reconcile.Result{}, fmt.Errorf("failed to get kubeconfig for ClusterProfile=%s: %w", key, err) } + // already engaged and kubeconfig is not changed? + if _, ok := p.clusters[key]; ok { + if p.kubeconfig[key] != nil && reflect.DeepEqual(p.kubeconfig[key], cfg) { + log.Info("ClusterProfile already engaged and kubeconfig is unchanged, skipping") + return reconcile.Result{}, nil + } + + log.Info("ClusterProfile already engaged but kubeconfig is changed, re-engaging the ClusterProfile") + // disengage existing cluster first if it exists. + if cancel, ok := p.cancelFns[key]; ok { + log.V(3).Info("Cancelling existing context for ClusterProfile") + cancel() + delete(p.clusters, key) + delete(p.cancelFns, key) + delete(p.kubeconfig, key) + } + } + // create cluster. cl, err := p.opts.NewCluster(ctx, clp, cfg, p.opts.ClusterOptions...) if err != nil { @@ -253,6 +337,7 @@ func (p *Provider) Reconcile(ctx context.Context, req reconcile.Request) (reconc // remember. p.clusters[key] = cl p.cancelFns[key] = cancel + p.kubeconfig[key] = cfg log.Info("Added new cluster for ClusterProfile") @@ -261,6 +346,7 @@ func (p *Provider) Reconcile(ctx context.Context, req reconcile.Request) (reconc log.Error(err, "failed to engage manager for ClusterProfile") delete(p.clusters, key) delete(p.cancelFns, key) + delete(p.kubeconfig, key) return reconcile.Result{}, err } From 28ad9790d768d8327ef7a573eecddcb8a361f3e5 Mon Sep 17 00:00:00 2001 From: Shingo Omura Date: Fri, 20 Jun 2025 23:42:40 +0900 Subject: [PATCH 05/10] Added test for cluster inventory API provider Signed-off-by: Shingo Omura --- hack/test-all.sh | 4 +- providers/cluster-inventory-api/go.mod | 9 + providers/cluster-inventory-api/go.sum | 1 - providers/cluster-inventory-api/provider.go | 2 +- .../cluster-inventory-api/provider_test.go | 415 ++++++++++++++++++ providers/cluster-inventory-api/suite_test.go | 108 +++++ 6 files changed, 536 insertions(+), 3 deletions(-) create mode 100644 providers/cluster-inventory-api/provider_test.go create mode 100644 providers/cluster-inventory-api/suite_test.go diff --git a/hack/test-all.sh b/hack/test-all.sh index 34d841c..7248d0d 100755 --- a/hack/test-all.sh +++ b/hack/test-all.sh @@ -25,7 +25,9 @@ if [[ -n ${ARTIFACTS:-} ]]; then fi result=0 -go test -v -race ${P_FLAG} ${MOD_OPT} ./... --ginkgo.fail-fast ${GINKGO_ARGS} || result=$? +go test -v -race ${P_FLAG} ${MOD_OPT} ./... --ginkgo.fail-fast ${GINKGO_ARGS} \ + && ( cd providers/cluster-inventory-api; go test -v -race ${P_FLAG} ${MOD_OPT} ./... --ginkgo.fail-fast ${GINKGO_ARGS} ) \ + || result=$? if [[ -n ${ARTIFACTS:-} ]]; then mkdir -p ${ARTIFACTS} diff --git a/providers/cluster-inventory-api/go.mod b/providers/cluster-inventory-api/go.mod index 395a5d8..7d64076 100644 --- a/providers/cluster-inventory-api/go.mod +++ b/providers/cluster-inventory-api/go.mod @@ -4,6 +4,8 @@ go 1.24.2 require ( github.com/go-logr/logr v1.4.2 + github.com/onsi/ginkgo/v2 v2.22.0 + github.com/onsi/gomega v1.36.1 k8s.io/api v0.33.0 k8s.io/apimachinery v0.33.0 k8s.io/client-go v0.33.0 @@ -14,19 +16,23 @@ require ( require ( github.com/beorn7/perks v1.0.1 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-logr/zapr v1.3.0 // indirect github.com/go-openapi/jsonpointer v0.21.1 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.1 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/gnostic-models v0.6.9 // indirect github.com/google/go-cmp v0.7.0 // indirect + github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db // indirect github.com/google/uuid v1.6.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -41,6 +47,8 @@ require ( github.com/prometheus/procfs v0.15.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/x448/float16 v0.8.4 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect golang.org/x/net v0.38.0 // indirect golang.org/x/oauth2 v0.28.0 // indirect golang.org/x/sync v0.12.0 // indirect @@ -48,6 +56,7 @@ require ( golang.org/x/term v0.30.0 // indirect golang.org/x/text v0.23.0 // indirect golang.org/x/time v0.11.0 // indirect + golang.org/x/tools v0.26.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/protobuf v1.36.5 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect diff --git a/providers/cluster-inventory-api/go.sum b/providers/cluster-inventory-api/go.sum index b14d621..be0e44a 100644 --- a/providers/cluster-inventory-api/go.sum +++ b/providers/cluster-inventory-api/go.sum @@ -69,7 +69,6 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/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 v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= diff --git a/providers/cluster-inventory-api/provider.go b/providers/cluster-inventory-api/provider.go index 077158e..a76d71b 100644 --- a/providers/cluster-inventory-api/provider.go +++ b/providers/cluster-inventory-api/provider.go @@ -318,7 +318,7 @@ func (p *Provider) Reconcile(ctx context.Context, req reconcile.Request) (reconc } for _, idx := range p.indexers { if err := cl.GetCache().IndexField(ctx, idx.object, idx.field, idx.extractValue); err != nil { - return reconcile.Result{}, fmt.Errorf("failed to index field %q for ClusterProfile=%s: %w", idx.field, key, err) + return reconcile.Result{}, fmt.Errorf("failed to index field %q for %s=%s: %w", idx.field, idx.object.GetObjectKind().GroupVersionKind().String(), key, err) } } clusterCtx, cancel := context.WithCancel(ctx) diff --git a/providers/cluster-inventory-api/provider_test.go b/providers/cluster-inventory-api/provider_test.go new file mode 100644 index 0000000..3a2e491 --- /dev/null +++ b/providers/cluster-inventory-api/provider_test.go @@ -0,0 +1,415 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package clusterinventoryapi + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + "strconv" + "time" + + "golang.org/x/sync/errgroup" + + authenticationv1 "k8s.io/api/authentication/v1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/client-go/rest" + "k8s.io/client-go/util/retry" + "k8s.io/utils/ptr" + + clusterinventoryv1alpha1 "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1" + + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + mcbuilder "sigs.k8s.io/multicluster-runtime/pkg/builder" + mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" + mcreconcile "sigs.k8s.io/multicluster-runtime/pkg/reconcile" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Provider Cluster Inventory API", Ordered, func() { + ctx, cancel := context.WithCancel(context.Background()) + g, ctx := errgroup.WithContext(ctx) + + const consumerName = "hub" + var localMgr manager.Manager + var provider *Provider + var mgr mcmanager.Manager + + var cliHub client.Client + + var cliMember client.Client + var profileMember *clusterinventoryv1alpha1.ClusterProfile + // var kubeConfigSecretMember corev1.Secret + // var sa1Member corev1.ServiceAccount + var sa1TokenMember string + // var sa2Member corev1.ServiceAccount + var sa2TokenMember string + + BeforeAll(func() { + var err error + cliHub, err = client.New(cfgHub, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + + cliMember, err = client.New(cfgMember, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + + By("Setting up the Local manager", func() { + localMgr, err = manager.New(cfgHub, manager.Options{}) + Expect(err).NotTo(HaveOccurred()) + }) + + By("Setting up the Provider", func() { + var err error + provider, err = New(localMgr, Options{ + ConsumerName: consumerName, + }) + Expect(err).NotTo(HaveOccurred()) + Expect(provider).NotTo(BeNil()) + }) + + By("Setting up the cluster-aware manager, with the provider to lookup clusters", func() { + var err error + mgr, err = mcmanager.New(cfgHub, provider, manager.Options{}) + Expect(err).NotTo(HaveOccurred()) + }) + + By("Setting up the controller feeding the animals", func() { + err := mcbuilder.ControllerManagedBy(mgr). + Named("fleet-configmap-controller"). + For(&corev1.ConfigMap{}). + Complete(mcreconcile.Func( + func(ctx context.Context, req mcreconcile.Request) (ctrl.Result, error) { + log := log.FromContext(ctx).WithValues("request", req.String()) + log.Info("Reconciling ConfigMap") + + cl, err := mgr.GetCluster(ctx, req.ClusterName) + if err != nil { + return reconcile.Result{}, fmt.Errorf("failed to get cluster: %w", err) + } + + // Feed the animal. + cm := &corev1.ConfigMap{} + if err := cl.GetClient().Get(ctx, req.NamespacedName, cm); err != nil { + if apierrors.IsNotFound(err) { + return reconcile.Result{}, nil + } + return reconcile.Result{}, fmt.Errorf("failed to get configmap: %w", err) + } + if cm.GetLabels()["type"] != "animal" { + return reconcile.Result{}, nil + } + + cm.Data = map[string]string{"stomach": "food"} + if err := cl.GetClient().Update(ctx, cm); err != nil { + return reconcile.Result{}, fmt.Errorf("failed to update configmap: %w", err) + } + log.Info("Fed the animal", "configmap", cm.Name) + return ctrl.Result{}, nil + }, + )) + Expect(err).NotTo(HaveOccurred()) + + By("Adding an index to the provider clusters", func() { + err := mgr.GetFieldIndexer().IndexField(ctx, &corev1.ConfigMap{}, "type", func(obj client.Object) []string { + return []string{obj.GetLabels()["type"]} + }) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + By("Starting the provider, cluster, manager, and controller", func() { + g.Go(func() error { + err := localMgr.Start(ctx) + return ignoreCanceled(err) + }) + g.Go(func() error { + err := provider.Run(ctx, mgr) + return ignoreCanceled(err) + }) + g.Go(func() error { + err := mgr.Start(ctx) + return ignoreCanceled(err) + }) + }) + + By("Setting up the ClusterProfile for member clusters", func() { + profileMember = &clusterinventoryv1alpha1.ClusterProfile{ + ObjectMeta: metav1.ObjectMeta{ + Name: "member", + Namespace: "default", + }, + Spec: clusterinventoryv1alpha1.ClusterProfileSpec{ + DisplayName: "member", + ClusterManager: clusterinventoryv1alpha1.ClusterManager{ + Name: "test", + }, + }, + } + Expect(cliHub.Create(ctx, profileMember)).To(Succeed()) + profileMember.Status.Conditions = append(profileMember.Status.Conditions, metav1.Condition{ + Type: clusterinventoryv1alpha1.ClusterConditionControlPlaneHealthy, + Status: metav1.ConditionTrue, + Reason: "Healthy", + Message: "Control plane is mocked as healthy", + LastTransitionTime: metav1.Now(), + }) + Expect(cliHub.Status().Update(ctx, profileMember)).To(Succeed()) + + _, sa1TokenMember = mustCreateAdminSAAndToken(ctx, cliMember, "sa1", "default") + _ = mustCreateOrUpdateKubeConfigSecretFromTokenSecret( + ctx, cliHub, cfgMember, + consumerName, + *profileMember, + sa1TokenMember, + ) + }) + + }) + + BeforeAll(func() { + runtime.Must(client.IgnoreAlreadyExists(cliMember.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "jungle"}}))) + runtime.Must(client.IgnoreAlreadyExists(cliMember.Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: "jungle", Name: "monkey", Labels: map[string]string{"type": "animal"}}}))) + runtime.Must(client.IgnoreAlreadyExists(cliMember.Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: "jungle", Name: "tree", Labels: map[string]string{"type": "thing"}}}))) + runtime.Must(client.IgnoreAlreadyExists(cliMember.Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: "jungle", Name: "tarzan", Labels: map[string]string{"type": "human"}}}))) + }) + + It("runs the reconciler for existing objects", func(ctx context.Context) { + Eventually(func() string { + lion := &corev1.ConfigMap{} + err := cliMember.Get(ctx, client.ObjectKey{Namespace: "jungle", Name: "monkey"}, lion) + Expect(err).NotTo(HaveOccurred()) + return lion.Data["stomach"] + }, "10s").Should(Equal("food")) + }) + + It("runs the reconciler for new objects", func(ctx context.Context) { + By("Creating a new configmap", func() { + err := cliMember.Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: "jungle", Name: "gorilla", Labels: map[string]string{"type": "animal"}}}) + Expect(err).NotTo(HaveOccurred()) + }) + + Eventually(func() string { + tiger := &corev1.ConfigMap{} + err := cliMember.Get(ctx, client.ObjectKey{Namespace: "jungle", Name: "gorilla"}, tiger) + Expect(err).NotTo(HaveOccurred()) + return tiger.Data["stomach"] + }, "10s").Should(Equal("food")) + }) + + It("runs the reconciler for updated objects", func(ctx context.Context) { + updated := &corev1.ConfigMap{} + By("Emptying the gorilla's stomach", func() { + err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + if err := cliMember.Get(ctx, client.ObjectKey{Namespace: "jungle", Name: "gorilla"}, updated); err != nil { + return err + } + updated.Data = map[string]string{} + return cliMember.Update(ctx, updated) + }) + Expect(err).NotTo(HaveOccurred()) + }) + rv, err := strconv.ParseInt(updated.ResourceVersion, 10, 64) + Expect(err).NotTo(HaveOccurred()) + + Eventually(func() int64 { + elephant := &corev1.ConfigMap{} + err := cliMember.Get(ctx, client.ObjectKey{Namespace: "jungle", Name: "gorilla"}, elephant) + Expect(err).NotTo(HaveOccurred()) + rv, err := strconv.ParseInt(elephant.ResourceVersion, 10, 64) + Expect(err).NotTo(HaveOccurred()) + return rv + }, "10s").Should(BeNumerically(">=", rv)) + + Eventually(func() string { + elephant := &corev1.ConfigMap{} + err := cliMember.Get(ctx, client.ObjectKey{Namespace: "jungle", Name: "gorilla"}, elephant) + Expect(err).NotTo(HaveOccurred()) + return elephant.Data["stomach"] + }, "10s").Should(Equal("food")) + }) + + It("queries one cluster via a multi-cluster index", func() { + cl, err := mgr.GetCluster(ctx, "default/member") + Expect(err).NotTo(HaveOccurred()) + + cms := &corev1.ConfigMapList{} + err = cl.GetCache().List(ctx, cms, client.MatchingFields{"type": "human"}) + Expect(err).NotTo(HaveOccurred()) + Expect(cms.Items).To(HaveLen(1)) + Expect(cms.Items[0].Name).To(Equal("tarzan")) + Expect(cms.Items[0].Namespace).To(Equal("jungle")) + }) + + It("queries all clusters via a multi-cluster index with a namespace", func() { + cl, err := mgr.GetCluster(ctx, "default/member") + Expect(err).NotTo(HaveOccurred()) + cms := &corev1.ConfigMapList{} + err = cl.GetCache().List(ctx, cms, client.InNamespace("jungle"), client.MatchingFields{"type": "human"}) + Expect(err).NotTo(HaveOccurred()) + Expect(cms.Items).To(HaveLen(1)) + Expect(cms.Items[0].Name).To(Equal("tarzan")) + Expect(cms.Items[0].Namespace).To(Equal("jungle")) + }) + + It("re-engages the cluster when kubeconfig of the cluster profile changes", func(ctx context.Context) { + By("Update the kubeconfig for the member ClusterProfile", func() { + _, sa2TokenMember = mustCreateAdminSAAndToken(ctx, cliMember, "sa2", "default") + _ = mustCreateOrUpdateKubeConfigSecretFromTokenSecret( + ctx, cliHub, cfgMember, + consumerName, + *profileMember, + sa2TokenMember, + ) + }) + + By("runs the reconciler for new objects(i.e. waiting for the reconciler to re-engage the cluster)", func() { + time.Sleep(2 * time.Second) // Give some time for the reconciler to pick up the new kubeconfig + jaguar := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "jungle", + Name: "jaguar", + Labels: map[string]string{"type": "animal"}, + }, + } + Expect(cliMember.Create(ctx, jaguar)).NotTo(HaveOccurred()) + Eventually(func(g Gomega) string { + err := cliMember.Get(ctx, client.ObjectKey{Namespace: "jungle", Name: "jaguar"}, jaguar) + g.Expect(err).NotTo(HaveOccurred()) + return jaguar.Data["stomach"] + }, "10s").Should(Equal("food")) + }) + }) + + AfterAll(func() { + By("Stopping the provider, cluster, manager, and controller", func() { + cancel() + }) + By("Waiting for the error group to finish", func() { + err := g.Wait() + Expect(err).NotTo(HaveOccurred()) + }) + }) +}) + +func ignoreCanceled(err error) error { + if errors.Is(err, context.Canceled) { + return nil + } + return err +} + +func mustCreateAdminSAAndToken(ctx context.Context, cli client.Client, name, namespace string) (corev1.ServiceAccount, string) { + sa := corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + } + Expect(cli.Create(ctx, &sa)).To(Succeed()) + + tokenRequest := authenticationv1.TokenRequest{ + Spec: authenticationv1.TokenRequestSpec{ + Audiences: []string{}, + ExpirationSeconds: ptr.To(int64(86400)), // 1 day + }, + } + Expect(cli.SubResource("token").Create(ctx, &sa, &tokenRequest)).NotTo(HaveOccurred()) + + adminClusterRoleBinding := rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: name + "-admin", + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: sa.Name, + Namespace: sa.Namespace, + }, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "ClusterRole", + Name: "cluster-admin", + }, + } + Expect(cli.Create(ctx, &adminClusterRoleBinding)).To(Succeed()) + + return sa, tokenRequest.Status.Token +} + +func mustCreateOrUpdateKubeConfigSecretFromTokenSecret( + ctx context.Context, + cli client.Client, + cfg *rest.Config, + consumerName string, + clusterProfile clusterinventoryv1alpha1.ClusterProfile, + token string, +) corev1.Secret { + kubeconfigStr := fmt.Sprintf(`apiVersion: v1 +clusters: +- cluster: + certificate-authority-data: |- + %s + server: %s + name: cluster +contexts: +- context: + cluster: cluster + user: user + name: cluster +current-context: cluster +kind: Config +users: +- name: user + user: + token: |- + %s +`, base64.StdEncoding.EncodeToString(cfg.CAData), cfg.Host, token) + + kubeConfigSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-kubeconfig", clusterProfile.Name), + Namespace: "default", + }, + } + + _, err := controllerutil.CreateOrUpdate(ctx, cli, kubeConfigSecret, func() error { + kubeConfigSecret.Labels = map[string]string{ + labelKeyClusterProfile: clusterProfile.Name, + labelKeyClusterInventoryConsumer: consumerName, + } + kubeConfigSecret.StringData = map[string]string{ + dataKeyKubeConfig: kubeconfigStr, + } + return nil + }) + Expect(err).NotTo(HaveOccurred()) + return *kubeConfigSecret +} diff --git a/providers/cluster-inventory-api/suite_test.go b/providers/cluster-inventory-api/suite_test.go new file mode 100644 index 0000000..21b2743 --- /dev/null +++ b/providers/cluster-inventory-api/suite_test.go @@ -0,0 +1,108 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package clusterinventoryapi + +import ( + "io" + "net/http" + "os" + "path/filepath" + "testing" + + "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + + clusterinventoryv1alpha1 "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestBuilder(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Cluster Inventory API Provider Suite") +} + +var testenvHub *envtest.Environment +var cfgHub *rest.Config + +var testenvMember *envtest.Environment +var cfgMember *rest.Config + +var _ = BeforeSuite(func() { + runtime.Must(clusterinventoryv1alpha1.AddToScheme(scheme.Scheme)) + + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + assetDir := GinkgoT().TempDir() + + clusterProfileCRDPath := filepath.Join(assetDir, "multicluster.x-k8s.io_clusterprofiles.yaml") + Expect(DownloadFile( + clusterProfileCRDPath, + "https://raw.githubusercontent.com/kubernetes-sigs/cluster-inventory-api/refs/heads/main/config/crd/bases/multicluster.x-k8s.io_clusterprofiles.yaml", + )).NotTo(HaveOccurred()) + + testenvHub = &envtest.Environment{ + ErrorIfCRDPathMissing: true, + CRDDirectoryPaths: []string{clusterProfileCRDPath}, + } + testenvMember = &envtest.Environment{} + + var err error + cfgHub, err = testenvHub.Start() + Expect(err).NotTo(HaveOccurred()) + cfgMember, err = testenvMember.Start() + Expect(err).NotTo(HaveOccurred()) + + // Prevent the metrics listener being created + metricsserver.DefaultBindAddress = "0" +}) + +var _ = AfterSuite(func() { + if testenvHub != nil { + Expect(testenvHub.Stop()).To(Succeed()) + } + if testenvMember != nil { + Expect(testenvMember.Stop()).To(Succeed()) + } + + // Put the DefaultBindAddress back + metricsserver.DefaultBindAddress = ":8080" +}) + +func DownloadFile(filepath string, url string) error { + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + out, err := os.Create(filepath) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, resp.Body) + return err +} From b35387bfaa703faad37c82cbab595769186b77a4 Mon Sep 17 00:00:00 2001 From: Shingo Omura Date: Sat, 21 Jun 2025 00:13:46 +0900 Subject: [PATCH 06/10] Introduce KubeconfigStrategy in cluster-inventory-api provider Signed-off-by: Shingo Omura --- providers/cluster-inventory-api/provider.go | 143 +++++++++++--------- 1 file changed, 76 insertions(+), 67 deletions(-) diff --git a/providers/cluster-inventory-api/provider.go b/providers/cluster-inventory-api/provider.go index a76d71b..2b4363a 100644 --- a/providers/cluster-inventory-api/provider.go +++ b/providers/cluster-inventory-api/provider.go @@ -54,8 +54,19 @@ var _ multicluster.Provider = &Provider{} const ( labelKeyClusterInventoryConsumer = "x-k8s.io/cluster-inventory-consumer" labelKeyClusterProfile = "x-k8s.io/cluster-profile" + dataKeyKubeConfig = "Config" // data key in the Secret that contains the kubeconfig. ) +// KubeconfigStrategy defines how the kubeconfig for a cluster profile is managed. +// It is used to fetch the kubeconfig for a cluster profile and can be extended to support different strategies. +type KubeconfigStrategy struct { + // GetKubeConfig is a function that returns the kubeconfig secret for a cluster profile. + GetKubeConfig func(ctx context.Context, cli client.Client, clp *clusterinventoryv1alpha1.ClusterProfile) (*rest.Config, error) + + // CustomWatches can add custom watches to the provider controller + CustomWatches []CustomWatch +} + // Options are the options for the Cluster-API cluster Provider. type Options struct { // ConsumerName is the name of the consumer that will use the cluster inventory API. @@ -64,15 +75,17 @@ type Options struct { // ClusterOptions are the options passed to the cluster constructor. ClusterOptions []cluster.Option - // GetKubeConfig is a function that returns the kubeconfig secret for a cluster profile. - GetKubeConfig func(ctx context.Context, cli client.Client, clp *clusterinventoryv1alpha1.ClusterProfile) (*rest.Config, error) + // KubeconfigStrategy defines how the kubeconfig for the cluster profile is managed. + // It is used to fetch the kubeconfig for a cluster profile and can be extended to support different strategies. + // The default strategy is KubeconfigStrategySecret(consumerName) which fetches the kubeconfig from a Secret + // labeled with "x-k8s.io/cluster-inventory-consumer" and "x-k8s.io/cluster-profile" labels. + // This is the "Push Model via Credentials in Secret" as described in KEP-4322: ClusterProfile API. + // ref: https://github.com/kubernetes/enhancements/blob/master/keps/sig-multicluster/4322-cluster-inventory/README.md#push-model-via-credentials-in-secret-not-recommended + KubeconfigStrategy *KubeconfigStrategy // NewCluster is a function that creates a new cluster from a rest.Config. // The cluster will be started by the provider. NewCluster func(ctx context.Context, clp *clusterinventoryv1alpha1.ClusterProfile, cfg *rest.Config, opts ...cluster.Option) (cluster.Cluster, error) - - // CustomWatches can add custom watches to the provider controller - CustomWatches []CustomWatch } // CustomWatch specifies a custom watch spec that can be added to the provider controller. @@ -102,79 +115,75 @@ type Provider struct { indexers []index } -// GetKubeConfigFromSecret returns a function that fetches the kubeconfig for a specified consumer for ClusterProfile from Secret -// It supposes that the Secrets for ClusterProfiles are managed by following "Push Model via Credentials in Secret" in "KEP-4322: ClusterProfile API" +// KubeconfigManagementStrategySecret returns a KubeconfigStrategy that fetches the kubeconfig from a Secret +// labeled with "x-k8s.io/cluster-inventory-consumer" and "x-k8s.io/cluster-profile" labels. +// This is the "Push Model via Credentials in Secret" as described in KEP-4322: ClusterProfile API. // ref: https://github.com/kubernetes/enhancements/blob/master/keps/sig-multicluster/4322-cluster-inventory/README.md#push-model-via-credentials-in-secret-not-recommended -func GetKubeConfigFromSecret(consumerName string) func(ctx context.Context, cli client.Client, clp *clusterinventoryv1alpha1.ClusterProfile) (*rest.Config, error) { - return func(ctx context.Context, cli client.Client, clp *clusterinventoryv1alpha1.ClusterProfile) (*rest.Config, error) { - secrets := corev1.SecretList{} - if err := cli.List(ctx, &secrets, client.InNamespace(clp.Namespace), client.MatchingLabels{ - labelKeyClusterInventoryConsumer: consumerName, - labelKeyClusterProfile: clp.Name, - }); err != nil { - return nil, fmt.Errorf("failed to list secrets: %w", err) - } - - if len(secrets.Items) == 0 { - return nil, fmt.Errorf("no secrets found") - } +func KubeconfigStrategySecret(consumerName string) *KubeconfigStrategy { + return &KubeconfigStrategy{ + GetKubeConfig: func(ctx context.Context, cli client.Client, clp *clusterinventoryv1alpha1.ClusterProfile) (*rest.Config, error) { + secrets := corev1.SecretList{} + if err := cli.List(ctx, &secrets, client.InNamespace(clp.Namespace), client.MatchingLabels{ + labelKeyClusterInventoryConsumer: consumerName, + labelKeyClusterProfile: clp.Name, + }); err != nil { + return nil, fmt.Errorf("failed to list secrets: %w", err) + } - if len(secrets.Items) > 1 { - return nil, fmt.Errorf("multiple secrets found, expected one, got %d", len(secrets.Items)) - } + if len(secrets.Items) == 0 { + return nil, fmt.Errorf("no secrets found") + } - secret := secrets.Items[0] + if len(secrets.Items) > 1 { + return nil, fmt.Errorf("multiple secrets found, expected one, got %d", len(secrets.Items)) + } - data, ok := secret.Data["Config"] - if !ok { - return nil, fmt.Errorf("secret %s/%s does not contain Config data", secret.Namespace, secret.Name) - } - return clientcmd.RESTConfigFromKubeConfig(data) - } -} + secret := secrets.Items[0] -// WatchKubeConfigSecret returns a CustomWatch that watches for kubeconfig secrets for specified consumer of ClusterProfile -// It supposes that the Secrets for ClusterProfiles are managed by following "Push Model via Credentials in Secret" in "KEP-4322: ClusterProfile API" -// ref: https://github.com/kubernetes/enhancements/blob/master/keps/sig-multicluster/4322-cluster-inventory/README.md#push-model-via-credentials-in-secret-not-recommended -func WatchKubeConfigSecret(consumerName string) CustomWatch { - return CustomWatch{ - Object: &corev1.Secret{}, - EventHandler: handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request { - secret, ok := obj.(*corev1.Secret) + data, ok := secret.Data[dataKeyKubeConfig] if !ok { - return nil + return nil, fmt.Errorf("secret %s/%s does not contain Config data", secret.Namespace, secret.Name) } - - if secret.GetLabels() == nil || - secret.GetLabels()[labelKeyClusterInventoryConsumer] != consumerName || - secret.GetLabels()[labelKeyClusterProfile] == "" { - return nil - } - - return []reconcile.Request{{ - NamespacedName: types.NamespacedName{ - Namespace: secret.GetNamespace(), - Name: secret.GetLabels()[labelKeyClusterProfile], - }, - }} - }), - Opts: []builder.WatchesOption{ - builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool { - secret, ok := object.(*corev1.Secret) + return clientcmd.RESTConfigFromKubeConfig(data) + }, + CustomWatches: []CustomWatch{CustomWatch{ + Object: &corev1.Secret{}, + EventHandler: handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request { + secret, ok := obj.(*corev1.Secret) if !ok { - return false + return nil } - return secret.GetLabels()[labelKeyClusterInventoryConsumer] == consumerName && - secret.GetLabels()[labelKeyClusterProfile] != "" - })), - }, + + if secret.GetLabels() == nil || + secret.GetLabels()[labelKeyClusterInventoryConsumer] != consumerName || + secret.GetLabels()[labelKeyClusterProfile] == "" { + return nil + } + + return []reconcile.Request{{ + NamespacedName: types.NamespacedName{ + Namespace: secret.GetNamespace(), + Name: secret.GetLabels()[labelKeyClusterProfile], + }, + }} + }), + Opts: []builder.WatchesOption{ + builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool { + secret, ok := object.(*corev1.Secret) + if !ok { + return false + } + return secret.GetLabels()[labelKeyClusterInventoryConsumer] == consumerName && + secret.GetLabels()[labelKeyClusterProfile] != "" + })), + }, + }}, } } func setDefaults(opts *Options, cli client.Client) { - if opts.GetKubeConfig == nil { - opts.GetKubeConfig = GetKubeConfigFromSecret(opts.ConsumerName) - opts.CustomWatches = append(opts.CustomWatches, WatchKubeConfigSecret(opts.ConsumerName)) + if opts.KubeconfigStrategy == nil { + opts.KubeconfigStrategy = KubeconfigStrategySecret(opts.ConsumerName) } if opts.NewCluster == nil { opts.NewCluster = func(ctx context.Context, clp *clusterinventoryv1alpha1.ClusterProfile, cfg *rest.Config, opts ...cluster.Option) (cluster.Cluster, error) { @@ -202,7 +211,7 @@ func New(localMgr manager.Manager, opts Options) (*Provider, error) { WithOptions(controller.Options{MaxConcurrentReconciles: 1}) // no parallelism. // Apply any custom watches provided by the user - for _, customWatch := range p.opts.CustomWatches { + for _, customWatch := range p.opts.KubeconfigStrategy.CustomWatches { controllerBuilder.Watches( customWatch.Object, customWatch.EventHandler, @@ -286,7 +295,7 @@ func (p *Provider) Reconcile(ctx context.Context, req reconcile.Request) (reconc } // get kubeconfig - cfg, err := p.opts.GetKubeConfig(ctx, p.client, clp) + cfg, err := p.opts.KubeconfigStrategy.GetKubeConfig(ctx, p.client, clp) if err != nil { log.Error(err, "Failed to get kubeconfig for ClusterProfile") return reconcile.Result{}, fmt.Errorf("failed to get kubeconfig for ClusterProfile=%s: %w", key, err) From 058033fcc233d0e24da8ad060ea2c5d3923eaa8f Mon Sep 17 00:00:00 2001 From: Shingo Omura Date: Sat, 21 Jun 2025 00:44:34 +0900 Subject: [PATCH 07/10] delete unnecessary comments in test Signed-off-by: Shingo Omura --- providers/cluster-inventory-api/provider_test.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/providers/cluster-inventory-api/provider_test.go b/providers/cluster-inventory-api/provider_test.go index 3a2e491..dbfa709 100644 --- a/providers/cluster-inventory-api/provider_test.go +++ b/providers/cluster-inventory-api/provider_test.go @@ -66,10 +66,7 @@ var _ = Describe("Provider Cluster Inventory API", Ordered, func() { var cliMember client.Client var profileMember *clusterinventoryv1alpha1.ClusterProfile - // var kubeConfigSecretMember corev1.Secret - // var sa1Member corev1.ServiceAccount var sa1TokenMember string - // var sa2Member corev1.ServiceAccount var sa2TokenMember string BeforeAll(func() { @@ -173,6 +170,7 @@ var _ = Describe("Provider Cluster Inventory API", Ordered, func() { }, } Expect(cliHub.Create(ctx, profileMember)).To(Succeed()) + // Mock the control plane health condition profileMember.Status.Conditions = append(profileMember.Status.Conditions, metav1.Condition{ Type: clusterinventoryv1alpha1.ClusterConditionControlPlaneHealthy, Status: metav1.ConditionTrue, From b23922a05a4204fd03b43c60cd72e4a102f2bb8b Mon Sep 17 00:00:00 2001 From: Shingo Omura Date: Tue, 24 Jun 2025 22:44:00 +0900 Subject: [PATCH 08/10] multicluster-runtime should be "replace" in go.mod Signed-off-by: Shingo Omura --- providers/cluster-inventory-api/go.mod | 6 ++++-- providers/cluster-inventory-api/go.sum | 2 -- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/providers/cluster-inventory-api/go.mod b/providers/cluster-inventory-api/go.mod index 7d64076..3a53d08 100644 --- a/providers/cluster-inventory-api/go.mod +++ b/providers/cluster-inventory-api/go.mod @@ -2,13 +2,17 @@ module sigs.k8s.io/multicluster-runtime/providers/cluster-inventory-api go 1.24.2 +replace sigs.k8s.io/multicluster-runtime => ../.. + require ( github.com/go-logr/logr v1.4.2 github.com/onsi/ginkgo/v2 v2.22.0 github.com/onsi/gomega v1.36.1 + golang.org/x/sync v0.12.0 k8s.io/api v0.33.0 k8s.io/apimachinery v0.33.0 k8s.io/client-go v0.33.0 + k8s.io/utils v0.0.0-20241210054802-24370beab758 sigs.k8s.io/cluster-inventory-api v0.0.0-20250318031555-c7c0594aa53b sigs.k8s.io/controller-runtime v0.21.0 sigs.k8s.io/multicluster-runtime v0.21.0-alpha.8 @@ -51,7 +55,6 @@ require ( go.uber.org/zap v1.27.0 // indirect golang.org/x/net v0.38.0 // indirect golang.org/x/oauth2 v0.28.0 // indirect - golang.org/x/sync v0.12.0 // indirect golang.org/x/sys v0.31.0 // indirect golang.org/x/term v0.30.0 // indirect golang.org/x/text v0.23.0 // indirect @@ -65,7 +68,6 @@ require ( k8s.io/apiextensions-apiserver v0.33.0 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect - k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect diff --git a/providers/cluster-inventory-api/go.sum b/providers/cluster-inventory-api/go.sum index be0e44a..ad8af6f 100644 --- a/providers/cluster-inventory-api/go.sum +++ b/providers/cluster-inventory-api/go.sum @@ -179,8 +179,6 @@ sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytI sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= -sigs.k8s.io/multicluster-runtime v0.21.0-alpha.8 h1:Pq69tTKfN8ADw8m8A3wUtP8wJ9SPQbbOsgapm3BZEPw= -sigs.k8s.io/multicluster-runtime v0.21.0-alpha.8/go.mod h1:CpBzLMLQKdm+UCchd2FiGPiDdCxM5dgCCPKuaQ6Fsv0= sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= From 3ff3014da531c106d40152dd193a673ddd4546d0 Mon Sep 17 00:00:00 2001 From: Shingo Omura Date: Tue, 24 Jun 2025 23:23:11 +0900 Subject: [PATCH 09/10] Use GetLocalManager() in multi-cluster manager instead of creating a new local manager Signed-off-by: Shingo Omura --- examples/cluster-inventory-api/main.go | 19 ++++----- providers/cluster-inventory-api/provider.go | 40 ++++++++++--------- .../cluster-inventory-api/provider_test.go | 24 ++++------- 3 files changed, 35 insertions(+), 48 deletions(-) diff --git a/examples/cluster-inventory-api/main.go b/examples/cluster-inventory-api/main.go index 1f46a8f..4ec86dc 100644 --- a/examples/cluster-inventory-api/main.go +++ b/examples/cluster-inventory-api/main.go @@ -58,14 +58,9 @@ func main() { entryLog.Error(err, "unable to get kubeconfig") os.Exit(1) } - localMgr, err := manager.New(cfg, manager.Options{}) - if err != nil { - entryLog.Error(err, "unable to set up overall controller manager") - os.Exit(1) - } // Create the provider against the local manager. - provider, err := clusterinventoryapi.New(localMgr, clusterinventoryapi.Options{ + provider := clusterinventoryapi.New(clusterinventoryapi.Options{ ConsumerName: "cluster-inventory-api-consumer", }) if err != nil { @@ -86,6 +81,12 @@ func main() { os.Exit(1) } + // Setting up the provider with multi-cluster manager. + if err := provider.SetupWithManager(mcMgr); err != nil { + entryLog.Error(err, "unable to set up provider with manager") + os.Exit(1) + } + // Create a configmap controller in the multi-cluster manager. if err := mcbuilder.ControllerManagedBy(mcMgr). Named("multicluster-configmaps"). @@ -119,12 +120,6 @@ func main() { // Starting everything. g, ctx := errgroup.WithContext(ctx) - g.Go(func() error { - return ignoreCanceled(localMgr.Start(ctx)) - }) - g.Go(func() error { - return ignoreCanceled(provider.Run(ctx, mcMgr)) - }) g.Go(func() error { return ignoreCanceled(mcMgr.Start(ctx)) }) diff --git a/providers/cluster-inventory-api/provider.go b/providers/cluster-inventory-api/provider.go index 2b4363a..246b7e3 100644 --- a/providers/cluster-inventory-api/provider.go +++ b/providers/cluster-inventory-api/provider.go @@ -41,7 +41,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -193,17 +192,32 @@ func setDefaults(opts *Options, cli client.Client) { } // New creates a new Cluster Inventory API cluster Provider. -func New(localMgr manager.Manager, opts Options) (*Provider, error) { +// You must call SetupWithManager to set up the provider with the manager. +func New(opts Options) *Provider { p := &Provider{ opts: opts, log: log.Log.WithName("cluster-inventory-api-cluster-provider"), - client: localMgr.GetClient(), clusters: map[string]cluster.Cluster{}, cancelFns: map[string]context.CancelFunc{}, kubeconfig: map[string]*rest.Config{}, } - setDefaults(&p.opts, p.client) + return p +} + +// SetupWithManager sets up the provider with the manager. +func (p *Provider) SetupWithManager(mgr mcmanager.Manager) error { + if mgr == nil { + return fmt.Errorf("manager is nil") + } + p.mcMgr = mgr + + // Get the local manager from the multi-cluster manager. + localMgr := mgr.GetLocalManager() + if localMgr == nil { + return fmt.Errorf("local manager is nil") + } + p.client = localMgr.GetClient() // Create a controller builder controllerBuilder := builder.ControllerManagedBy(localMgr). @@ -221,10 +235,10 @@ func New(localMgr manager.Manager, opts Options) (*Provider, error) { // Complete the controller setup if err := controllerBuilder.Complete(p); err != nil { - return nil, fmt.Errorf("failed to create controller: %w", err) + return fmt.Errorf("failed to create controller: %w", err) } - return p, nil + return nil } // Get returns the cluster with the given name, if it is known. @@ -238,19 +252,7 @@ func (p *Provider) Get(_ context.Context, clusterName string) (cluster.Cluster, return nil, multicluster.ErrClusterNotFound } -// Run starts the provider and blocks. -func (p *Provider) Run(ctx context.Context, mgr mcmanager.Manager) error { - p.log.Info("Starting Cluster Inventory API cluster provider") - - p.lock.Lock() - p.mcMgr = mgr - p.lock.Unlock() - - <-ctx.Done() - - return ctx.Err() -} - +// Reconcile is the reconcile loop for the Cluster Inventory API cluster Provider. func (p *Provider) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { key := req.NamespacedName.String() diff --git a/providers/cluster-inventory-api/provider_test.go b/providers/cluster-inventory-api/provider_test.go index dbfa709..bc1f1ef 100644 --- a/providers/cluster-inventory-api/provider_test.go +++ b/providers/cluster-inventory-api/provider_test.go @@ -58,7 +58,6 @@ var _ = Describe("Provider Cluster Inventory API", Ordered, func() { g, ctx := errgroup.WithContext(ctx) const consumerName = "hub" - var localMgr manager.Manager var provider *Provider var mgr mcmanager.Manager @@ -77,17 +76,10 @@ var _ = Describe("Provider Cluster Inventory API", Ordered, func() { cliMember, err = client.New(cfgMember, client.Options{}) Expect(err).NotTo(HaveOccurred()) - By("Setting up the Local manager", func() { - localMgr, err = manager.New(cfgHub, manager.Options{}) - Expect(err).NotTo(HaveOccurred()) - }) - By("Setting up the Provider", func() { - var err error - provider, err = New(localMgr, Options{ + provider = New(Options{ ConsumerName: consumerName, }) - Expect(err).NotTo(HaveOccurred()) Expect(provider).NotTo(BeNil()) }) @@ -97,6 +89,11 @@ var _ = Describe("Provider Cluster Inventory API", Ordered, func() { Expect(err).NotTo(HaveOccurred()) }) + By("Setting up the provider controller", func() { + err := provider.SetupWithManager(mgr) + Expect(err).NotTo(HaveOccurred()) + }) + By("Setting up the controller feeding the animals", func() { err := mcbuilder.ControllerManagedBy(mgr). Named("fleet-configmap-controller"). @@ -142,14 +139,7 @@ var _ = Describe("Provider Cluster Inventory API", Ordered, func() { }) By("Starting the provider, cluster, manager, and controller", func() { - g.Go(func() error { - err := localMgr.Start(ctx) - return ignoreCanceled(err) - }) - g.Go(func() error { - err := provider.Run(ctx, mgr) - return ignoreCanceled(err) - }) + g.Go(func() error { err := mgr.Start(ctx) return ignoreCanceled(err) From 144110c9f7de1ce04de7350456102d8b3d1add7e Mon Sep 17 00:00:00 2001 From: Shingo Omura Date: Tue, 24 Jun 2025 23:25:28 +0900 Subject: [PATCH 10/10] fix typo Signed-off-by: Shingo Omura --- providers/cluster-inventory-api/provider.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/providers/cluster-inventory-api/provider.go b/providers/cluster-inventory-api/provider.go index 246b7e3..9290372 100644 --- a/providers/cluster-inventory-api/provider.go +++ b/providers/cluster-inventory-api/provider.go @@ -114,7 +114,7 @@ type Provider struct { indexers []index } -// KubeconfigManagementStrategySecret returns a KubeconfigStrategy that fetches the kubeconfig from a Secret +// KubeconfigStrategySecret returns a KubeconfigStrategy that fetches the kubeconfig from a Secret // labeled with "x-k8s.io/cluster-inventory-consumer" and "x-k8s.io/cluster-profile" labels. // This is the "Push Model via Credentials in Secret" as described in KEP-4322: ClusterProfile API. // ref: https://github.com/kubernetes/enhancements/blob/master/keps/sig-multicluster/4322-cluster-inventory/README.md#push-model-via-credentials-in-secret-not-recommended