From eb8925802bade1cd8c4e689dbcf2ee966c608789 Mon Sep 17 00:00:00 2001 From: Shingo Omura Date: Thu, 19 Jun 2025 22:15:04 +0900 Subject: [PATCH 01/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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 From 7670aa1ccfbac007a9a9f05f516ce79398db06dc Mon Sep 17 00:00:00 2001 From: Shingo Omura Date: Fri, 25 Jul 2025 21:00:57 +0900 Subject: [PATCH 11/18] add module definition in Makefile Signed-off-by: Shingo Omura --- Makefile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Makefile b/Makefile index bc7097a..1128dc0 100644 --- a/Makefile +++ b/Makefile @@ -56,6 +56,8 @@ EXAMPLES_KIND_DIR := $(abspath examples/kind) PROVIDERS_KIND_DIR := $(abspath providers/kind) EXAMPLES_CLUSTER_API_DIR := $(abspath examples/cluster-api) PROVIDERS_CLUSTER_API_DIR := $(abspath providers/cluster-api) +EXAMPLES_CLUSTER_INVENTORY_API_DIR := $(abspath examples/cluster-inventory-api) +PROVIDERS_CLUSTER_INVENTORY_API_DIR := $(abspath providers/cluster-inventory-api) GO_INSTALL := ./hack/go-install.sh # The help will print out all targets with their descriptions organized bellow their categories. The categories are represented by `##@` and the target descriptions by `##`. @@ -140,6 +142,8 @@ modules: ## Runs go mod to ensure modules are up to date. cd $(PROVIDERS_KIND_DIR); go mod tidy cd $(EXAMPLES_CLUSTER_API_DIR); go mod tidy cd $(PROVIDERS_CLUSTER_API_DIR); go mod tidy + cd $(EXAMPLES_CLUSTER_INVENTORY_API_DIR); go mod tidy + cd $(PROVIDERS_CLUSTER_INVENTORY_API_DIR); go mod tidy ## -------------------------------------- ## Cleanup / Verification @@ -165,6 +169,8 @@ verify-modules: modules $(GO_MOD_CHECK) ## Verify go modules are up to date $(PROVIDERS_KIND_DIR)/go.mod $(PROVIDERS_KIND_DIR)/go.sum \ $(EXAMPLES_CLUSTER_API_DIR)/go.mod $(EXAMPLES_CLUSTER_API_DIR)/go.sum \ $(PROVIDERS_CLUSTER_API_DIR)/go.mod $(PROVIDERS_CLUSTER_API_DIR)/go.sum \ + $(EXAMPLES_CLUSTER_INVENTORY_API_DIR)/go.mod $(EXAMPLES_CLUSTER_INVENTORY_API_DIR)/go.sum \ + $(PROVIDERS_CLUSTER_INVENTORY_API_DIR)/go.mod $(PROVIDERS_CLUSTER_INVENTORY_API_DIR)/go.sum \ ); then \ git diff; \ echo "go module files are out of date, please run 'make modules'"; exit 1; \ From 08707d85d2ff253f5a6641262b10cc96db6d800f Mon Sep 17 00:00:00 2001 From: Shingo Omura Date: Fri, 25 Jul 2025 20:59:37 +0900 Subject: [PATCH 12/18] Introduced kubeconfig strategy for cluster inventory API provider which can abstract how kubeconfigs are managed and retrieved. Signed-off-by: Shingo Omura --- examples/cluster-inventory-api/go.mod | 18 +-- examples/cluster-inventory-api/go.sum | 40 +++--- examples/cluster-inventory-api/main.go | 11 +- providers/cluster-inventory-api/go.mod | 22 +-- providers/cluster-inventory-api/go.sum | 44 +++--- .../kubeconfigstrategy/factory.go | 14 ++ .../kubeconfigstrategy/interface.go | 31 +++++ .../kubeconfigstrategy/secret.go | 110 +++++++++++++++ providers/cluster-inventory-api/provider.go | 130 +++--------------- .../cluster-inventory-api/provider_test.go | 18 ++- 10 files changed, 256 insertions(+), 182 deletions(-) create mode 100644 providers/cluster-inventory-api/kubeconfigstrategy/factory.go create mode 100644 providers/cluster-inventory-api/kubeconfigstrategy/interface.go create mode 100644 providers/cluster-inventory-api/kubeconfigstrategy/secret.go diff --git a/examples/cluster-inventory-api/go.mod b/examples/cluster-inventory-api/go.mod index e5e7e04..561c717 100644 --- a/examples/cluster-inventory-api/go.mod +++ b/examples/cluster-inventory-api/go.mod @@ -12,7 +12,7 @@ require ( 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/cluster-inventory-api v0.0.0-20250702132726-0f613c6275a5 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 @@ -24,7 +24,7 @@ require ( 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/fsnotify/fsnotify v1.8.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 @@ -47,18 +47,18 @@ require ( 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/spf13/pflag v1.0.6 // 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/net v0.39.0 // indirect + golang.org/x/oauth2 v0.29.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/term v0.31.0 // indirect + golang.org/x/text v0.24.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 + google.golang.org/protobuf v1.36.6 // 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 diff --git a/examples/cluster-inventory-api/go.sum b/examples/cluster-inventory-api/go.sum index ae46415..ae644f6 100644 --- a/examples/cluster-inventory-api/go.sum +++ b/examples/cluster-inventory-api/go.sum @@ -14,8 +14,8 @@ github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8 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/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 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= @@ -88,8 +88,8 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg 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/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/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= @@ -115,10 +115,10 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn 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/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= +golang.org/x/oauth2 v0.29.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= @@ -127,30 +127,30 @@ 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/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= +golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= 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/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 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/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= 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= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 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= @@ -174,8 +174,8 @@ k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUy 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/cluster-inventory-api v0.0.0-20250702132726-0f613c6275a5 h1:gonrKU7V8WnTnsGs/QasvvgWbS+S5GwXWfuKv4357HM= +sigs.k8s.io/cluster-inventory-api v0.0.0-20250702132726-0f613c6275a5/go.mod h1:uHmRJn/DNc0ScvENOAIagQPfXdjJfuDB7IgZxCpSLMM= 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= diff --git a/examples/cluster-inventory-api/main.go b/examples/cluster-inventory-api/main.go index 4ec86dc..dcbf60b 100644 --- a/examples/cluster-inventory-api/main.go +++ b/examples/cluster-inventory-api/main.go @@ -40,7 +40,9 @@ import ( 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" + "sigs.k8s.io/multicluster-runtime/providers/cluster-inventory-api/kubeconfigstrategy" ) func init() { @@ -60,8 +62,13 @@ func main() { } // Create the provider against the local manager. - provider := clusterinventoryapi.New(clusterinventoryapi.Options{ - ConsumerName: "cluster-inventory-api-consumer", + provider, err := clusterinventoryapi.New(clusterinventoryapi.Options{ + KubeconfigStrategyOption: kubeconfigstrategy.Option{ + // Use the Secret strategy with a specific consumer name. + Secret: kubeconfigstrategy.SecretStrategyOption{ + ConsumerName: "cluster-inventory-api-consumer", + }, + }, }) if err != nil { entryLog.Error(err, "unable to create provider") diff --git a/providers/cluster-inventory-api/go.mod b/providers/cluster-inventory-api/go.mod index 3a53d08..313a405 100644 --- a/providers/cluster-inventory-api/go.mod +++ b/providers/cluster-inventory-api/go.mod @@ -8,12 +8,12 @@ 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 + golang.org/x/sync v0.13.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/cluster-inventory-api v0.0.0-20250702132726-0f613c6275a5 sigs.k8s.io/controller-runtime v0.21.0 sigs.k8s.io/multicluster-runtime v0.21.0-alpha.8 ) @@ -25,7 +25,7 @@ require ( 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/fsnotify/fsnotify v1.8.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 @@ -49,19 +49,19 @@ require ( 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/spf13/pflag v1.0.6 // 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/net v0.39.0 // indirect + golang.org/x/oauth2 v0.29.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/term v0.31.0 // indirect + golang.org/x/text v0.24.0 // indirect golang.org/x/time v0.11.0 // indirect - golang.org/x/tools v0.26.0 // indirect + golang.org/x/tools v0.30.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/protobuf v1.36.5 // indirect + google.golang.org/protobuf v1.36.6 // 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 diff --git a/providers/cluster-inventory-api/go.sum b/providers/cluster-inventory-api/go.sum index ad8af6f..15c1baa 100644 --- a/providers/cluster-inventory-api/go.sum +++ b/providers/cluster-inventory-api/go.sum @@ -14,8 +14,8 @@ github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8 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/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 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= @@ -87,8 +87,8 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg 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/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/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= @@ -114,42 +114,42 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn 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/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= +golang.org/x/oauth2 v0.29.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/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.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/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= +golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= 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/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 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/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= 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= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 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= @@ -173,8 +173,8 @@ k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUy 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/cluster-inventory-api v0.0.0-20250702132726-0f613c6275a5 h1:gonrKU7V8WnTnsGs/QasvvgWbS+S5GwXWfuKv4357HM= +sigs.k8s.io/cluster-inventory-api v0.0.0-20250702132726-0f613c6275a5/go.mod h1:uHmRJn/DNc0ScvENOAIagQPfXdjJfuDB7IgZxCpSLMM= 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= diff --git a/providers/cluster-inventory-api/kubeconfigstrategy/factory.go b/providers/cluster-inventory-api/kubeconfigstrategy/factory.go new file mode 100644 index 0000000..15ae8ca --- /dev/null +++ b/providers/cluster-inventory-api/kubeconfigstrategy/factory.go @@ -0,0 +1,14 @@ +package kubeconfigstrategy + +import "context" + +// Option specifies which strategy will be applied +type Option struct { + // Secret specifies option for the kubeconfig strategy based on a Secret. + Secret SecretStrategyOption +} + +// New creates a new kubeconfig strategy based on the provided options. +func New(ctx context.Context, option Option) (Interface, error) { + return newSecretKubeConfigStrategy(ctx, option.Secret) +} diff --git a/providers/cluster-inventory-api/kubeconfigstrategy/interface.go b/providers/cluster-inventory-api/kubeconfigstrategy/interface.go new file mode 100644 index 0000000..1a443c0 --- /dev/null +++ b/providers/cluster-inventory-api/kubeconfigstrategy/interface.go @@ -0,0 +1,31 @@ +package kubeconfigstrategy + +import ( + "context" + + "k8s.io/client-go/rest" + + clusterinventoryv1alpha1 "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1" + + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +// Interface defines how the kubeconfig for a cluster profile is managed. +// It is used to fetch the kubeconfig for a cluster profile. +type Interface interface { + // GetKubeConfig is a function that returns the kubeconfig secret for a cluster profile. + GetKubeConfig(ctx context.Context, cli client.Client, clp *clusterinventoryv1alpha1.ClusterProfile) (*rest.Config, 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 +} diff --git a/providers/cluster-inventory-api/kubeconfigstrategy/secret.go b/providers/cluster-inventory-api/kubeconfigstrategy/secret.go new file mode 100644 index 0000000..ce2d508 --- /dev/null +++ b/providers/cluster-inventory-api/kubeconfigstrategy/secret.go @@ -0,0 +1,110 @@ +package kubeconfigstrategy + +import ( + "context" + "fmt" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + + "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1" + + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +const ( + SecretLabelKeyClusterInventoryConsumer = "x-k8s.io/cluster-inventory-consumer" + SecretLabelKeyClusterProfile = "x-k8s.io/cluster-profile" + SecretDataKeyKubeConfig = "Config" // data key in the Secret that contains the kubeconfig. +) + +var _ Interface = &secretStrategy{} + +type secretStrategy struct { + consumerName string +} + +type SecretStrategyOption struct { + ConsumerName string +} + +func newSecretKubeConfigStrategy(ctx context.Context, option SecretStrategyOption) (Interface, error) { + if option.ConsumerName == "" { + return nil, fmt.Errorf("consumer name must be set for Secret strategy") + } + log.FromContext(ctx).Info("Using Secret strategy for for fetching kubeconfig from ClusterProfile", "consumerName", option.ConsumerName) + return &secretStrategy{ + consumerName: option.ConsumerName, + }, nil +} + +// CustomWatches implements Interface. +func (s *secretStrategy) CustomWatches() []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 + } + + if secret.GetLabels() == nil || + secret.GetLabels()[SecretLabelKeyClusterInventoryConsumer] != s.consumerName || + secret.GetLabels()[SecretLabelKeyClusterProfile] == "" { + return nil + } + + return []reconcile.Request{{ + NamespacedName: types.NamespacedName{ + Namespace: secret.GetNamespace(), + Name: secret.GetLabels()[SecretLabelKeyClusterProfile], + }, + }} + }), + Opts: []builder.WatchesOption{ + builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool { + secret, ok := object.(*corev1.Secret) + if !ok { + return false + } + return secret.GetLabels()[SecretLabelKeyClusterInventoryConsumer] == s.consumerName && + secret.GetLabels()[SecretLabelKeyClusterProfile] != "" + })), + }, + }} +} + +// GetKubeConfig implements Interface. +func (s *secretStrategy) GetKubeConfig(ctx context.Context, cli client.Client, clp *v1alpha1.ClusterProfile) (*rest.Config, error) { + secrets := corev1.SecretList{} + if err := cli.List(ctx, &secrets, client.InNamespace(clp.Namespace), client.MatchingLabels{ + SecretLabelKeyClusterInventoryConsumer: s.consumerName, + SecretLabelKeyClusterProfile: 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[SecretDataKeyKubeConfig] + if !ok { + return nil, fmt.Errorf("secret %s/%s does not contain Config data", secret.Namespace, secret.Name) + } + return clientcmd.RESTConfigFromKubeConfig(data) +} diff --git a/providers/cluster-inventory-api/provider.go b/providers/cluster-inventory-api/provider.go index 9290372..5bc68b5 100644 --- a/providers/cluster-inventory-api/provider.go +++ b/providers/cluster-inventory-api/provider.go @@ -27,73 +27,39 @@ import ( 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/apimachinery/pkg/types" "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/handler" "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/predicate" "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{} -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. + "sigs.k8s.io/multicluster-runtime/providers/cluster-inventory-api/kubeconfigstrategy" ) -// 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 -} +var _ multicluster.Provider = &Provider{} // 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 - // 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 + // KubeconfigStrategyOption specifies options for kubeconfig strategy + KubeconfigStrategyOption kubeconfigstrategy.Option // 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) } -// 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 @@ -106,6 +72,8 @@ type Provider struct { log logr.Logger client client.Client + strategy kubeconfigstrategy.Interface + lock sync.RWMutex mcMgr mcmanager.Manager clusters map[string]cluster.Cluster @@ -114,76 +82,7 @@ type Provider struct { indexers []index } -// 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 -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) == 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[dataKeyKubeConfig] - if !ok { - return nil, fmt.Errorf("secret %s/%s does not contain Config data", secret.Namespace, secret.Name) - } - 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 nil - } - - 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.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) { return cluster.New(cfg, opts...) @@ -193,16 +92,23 @@ func setDefaults(opts *Options, cli client.Client) { // New creates a new Cluster Inventory API cluster Provider. // You must call SetupWithManager to set up the provider with the manager. -func New(opts Options) *Provider { +func New(opts Options) (*Provider, error) { + logger := log.Log.WithName("cluster-inventory-api-cluster-provider") + ctx := log.IntoContext(context.Background(), logger) + strategy, err := kubeconfigstrategy.New(ctx, opts.KubeconfigStrategyOption) + if err != nil { + return nil, fmt.Errorf("failed to create kubeconfig strategy: %w", err) + } p := &Provider{ opts: opts, - log: log.Log.WithName("cluster-inventory-api-cluster-provider"), + log: logger, clusters: map[string]cluster.Cluster{}, cancelFns: map[string]context.CancelFunc{}, kubeconfig: map[string]*rest.Config{}, + strategy: strategy, } setDefaults(&p.opts, p.client) - return p + return p, nil } // SetupWithManager sets up the provider with the manager. @@ -225,7 +131,7 @@ func (p *Provider) SetupWithManager(mgr mcmanager.Manager) error { WithOptions(controller.Options{MaxConcurrentReconciles: 1}) // no parallelism. // Apply any custom watches provided by the user - for _, customWatch := range p.opts.KubeconfigStrategy.CustomWatches { + for _, customWatch := range p.strategy.CustomWatches() { controllerBuilder.Watches( customWatch.Object, customWatch.EventHandler, @@ -297,7 +203,7 @@ func (p *Provider) Reconcile(ctx context.Context, req reconcile.Request) (reconc } // get kubeconfig - cfg, err := p.opts.KubeconfigStrategy.GetKubeConfig(ctx, p.client, clp) + cfg, err := p.strategy.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) diff --git a/providers/cluster-inventory-api/provider_test.go b/providers/cluster-inventory-api/provider_test.go index bc1f1ef..075c283 100644 --- a/providers/cluster-inventory-api/provider_test.go +++ b/providers/cluster-inventory-api/provider_test.go @@ -48,12 +48,13 @@ import ( mcbuilder "sigs.k8s.io/multicluster-runtime/pkg/builder" mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" mcreconcile "sigs.k8s.io/multicluster-runtime/pkg/reconcile" + "sigs.k8s.io/multicluster-runtime/providers/cluster-inventory-api/kubeconfigstrategy" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) -var _ = Describe("Provider Cluster Inventory API", Ordered, func() { +var _ = Describe("Provider Cluster Inventory API With Secret Kubeconfig Strategy", Ordered, func() { ctx, cancel := context.WithCancel(context.Background()) g, ctx := errgroup.WithContext(ctx) @@ -77,9 +78,14 @@ var _ = Describe("Provider Cluster Inventory API", Ordered, func() { Expect(err).NotTo(HaveOccurred()) By("Setting up the Provider", func() { - provider = New(Options{ - ConsumerName: consumerName, + provider, err = New(Options{ + KubeconfigStrategyOption: kubeconfigstrategy.Option{ + Secret: kubeconfigstrategy.SecretStrategyOption{ + ConsumerName: consumerName, + }, + }, }) + Expect(err).NotTo(HaveOccurred()) Expect(provider).NotTo(BeNil()) }) @@ -390,11 +396,11 @@ users: _, err := controllerutil.CreateOrUpdate(ctx, cli, kubeConfigSecret, func() error { kubeConfigSecret.Labels = map[string]string{ - labelKeyClusterProfile: clusterProfile.Name, - labelKeyClusterInventoryConsumer: consumerName, + kubeconfigstrategy.SecretLabelKeyClusterProfile: clusterProfile.Name, + kubeconfigstrategy.SecretLabelKeyClusterInventoryConsumer: consumerName, } kubeConfigSecret.StringData = map[string]string{ - dataKeyKubeConfig: kubeconfigStr, + kubeconfigstrategy.SecretDataKeyKubeConfig: kubeconfigStr, } return nil }) From c5701e007ba65c63fc12e06f5ed938916dfe3f06 Mon Sep 17 00:00:00 2001 From: Huy Pham Date: Mon, 28 Jul 2025 23:03:53 +0900 Subject: [PATCH 13/18] feat: Add ClusterProfiles provider Co-authored-by: Shingo Omura Signed-off-by: Shingo Omura --- .../clusterprofile-provider-file.json | 14 + examples/clusterprofile/go.mod | 73 +++ examples/clusterprofile/go.sum | 196 ++++++++ examples/clusterprofile/main.go | 176 +++++++ .../clusterprofile_suite_test.go | 112 +++++ providers/clusterprofile/provider.go | 358 +++++++++++++++ providers/clusterprofile/provider_test.go | 431 ++++++++++++++++++ 7 files changed, 1360 insertions(+) create mode 100644 examples/clusterprofile/clusterprofile-provider-file.json create mode 100644 examples/clusterprofile/go.mod create mode 100644 examples/clusterprofile/go.sum create mode 100644 examples/clusterprofile/main.go create mode 100644 providers/clusterprofile/clusterprofile_suite_test.go create mode 100644 providers/clusterprofile/provider.go create mode 100644 providers/clusterprofile/provider_test.go diff --git a/examples/clusterprofile/clusterprofile-provider-file.json b/examples/clusterprofile/clusterprofile-provider-file.json new file mode 100644 index 0000000..21f90b2 --- /dev/null +++ b/examples/clusterprofile/clusterprofile-provider-file.json @@ -0,0 +1,14 @@ +{ + "providers": [ + { + "name": "google", + "execConfig": { + "apiVersion": "client.authentication.k8s.io/v1beta1", + "args": null, + "command": "gke-gcloud-auth-plugin", + "env": null, + "provideClusterInfo": true + } + } + ] +} \ No newline at end of file diff --git a/examples/clusterprofile/go.mod b/examples/clusterprofile/go.mod new file mode 100644 index 0000000..9689e71 --- /dev/null +++ b/examples/clusterprofile/go.mod @@ -0,0 +1,73 @@ +module sigs.k8s.io/multicluster-runtime/examples/clusterprofile + +go 1.24.0 + +replace ( + sigs.k8s.io/multicluster-runtime => ../.. + sigs.k8s.io/multicluster-runtime/providers/clusterprofile => ../../providers/clusterprofile +) + +require ( + k8s.io/api v0.33.3 + k8s.io/apimachinery v0.33.3 + k8s.io/kubectl v0.33.3 + sigs.k8s.io/cluster-inventory-api v0.0.0-20250702132726-0f613c6275a5 + sigs.k8s.io/controller-runtime v0.21.0 + sigs.k8s.io/multicluster-runtime 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.8.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-logr/logr v1.4.3 // 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.6 // 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.39.0 // indirect + golang.org/x/oauth2 v0.29.0 // indirect + golang.org/x/sync v0.15.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/term v0.31.0 // indirect + golang.org/x/text v0.24.0 // indirect + golang.org/x/time v0.11.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/protobuf v1.36.6 // 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/client-go v0.33.3 // 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/clusterprofile/go.sum b/examples/clusterprofile/go.sum new file mode 100644 index 0000000..57e84b0 --- /dev/null +++ b/examples/clusterprofile/go.sum @@ -0,0 +1,196 @@ +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.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +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.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/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/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +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.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/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.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= +golang.org/x/oauth2 v0.29.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.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= +golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= +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.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +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.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= +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.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +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/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +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.3 h1:SRd5t//hhkI1buzxb288fy2xvjubstenEKL9K51KBI8= +k8s.io/api v0.33.3/go.mod h1:01Y/iLUjNBM3TAvypct7DIj0M0NIZc+PzAHCIo0CYGE= +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.3 h1:4ZSrmNa0c/ZpZJhAgRdcsFcZOw1PQU1bALVQ0B3I5LA= +k8s.io/apimachinery v0.33.3/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/client-go v0.33.3 h1:M5AfDnKfYmVJif92ngN532gFqakcGi6RvaOF16efrpA= +k8s.io/client-go v0.33.3/go.mod h1:luqKBQggEf3shbxHY4uVENAxrDISLOarxpTKMiUuujg= +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/kubectl v0.33.3 h1:r/phHvH1iU7gO/l7tTjQk2K01ER7/OAJi8uFHHyWSac= +k8s.io/kubectl v0.33.3/go.mod h1:euj2bG56L6kUGOE/ckZbCoudPwuj4Kud7BR0GzyNiT0= +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-20250702132726-0f613c6275a5 h1:gonrKU7V8WnTnsGs/QasvvgWbS+S5GwXWfuKv4357HM= +sigs.k8s.io/cluster-inventory-api v0.0.0-20250702132726-0f613c6275a5/go.mod h1:uHmRJn/DNc0ScvENOAIagQPfXdjJfuDB7IgZxCpSLMM= +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/clusterprofile/main.go b/examples/clusterprofile/main.go new file mode 100644 index 0000000..7d11a55 --- /dev/null +++ b/examples/clusterprofile/main.go @@ -0,0 +1,176 @@ +/* +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. +*/ + +// This example demonstrates how to use the ClusterProfiles provider to create +// a multi-cluster operator that can watch and reconcile resources across +// multiple Kubernetes clusters using ClusterProfile resources. +// +// The operator watches for ClusterProfile resources in a specified namespace +// and automatically creates connections to the clusters they reference using +// configured credential providers. +// +// Usage: +// +// go run main.go --namespace cluster-inventory --credential-providers-file clusterprofile-provider-file.json +// +// This will: +// 1. Set up the ClusterProfiles provider with credential providers +// 2. Watch for ClusterProfile resources in the specified namespace +// 3. Create a multi-cluster controller that watches ConfigMaps across all connected clusters +// 4. Log information about discovered ConfigMaps +package main + +import ( + "context" + "errors" + "flag" + "log" + "os" + + "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/cluster-inventory-api/pkg/credentials" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + clusterinventory "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1" + ctrl "sigs.k8s.io/controller-runtime" + ctrllog "sigs.k8s.io/controller-runtime/pkg/log" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + mcbuilder "sigs.k8s.io/multicluster-runtime/pkg/builder" + mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" + mcreconcile "sigs.k8s.io/multicluster-runtime/pkg/reconcile" + clusterprofileprovider "sigs.k8s.io/multicluster-runtime/providers/clusterprofile" +) + +func init() { + runtime.Must(clusterinventory.AddToScheme(scheme.Scheme)) +} + +func main() { + // Set up command line flags for credential providers configuration + credentialsProviders := credentials.SetupProviderFileFlag() + flag.Parse() + + // Define the namespace where ClusterProfile resources will be watched + var clusterInventoryNamespace string + flag.StringVar(&clusterInventoryNamespace, "namespace", "", "Cluster inventory namespace") + + // Set up logging options + opts := zap.Options{ + Development: true, + } + opts.BindFlags(flag.CommandLine) + flag.Parse() + + // Initialize logger and signal handler + ctrllog.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + entryLog := ctrllog.Log.WithName("entrypoint") + ctx := ctrl.SetupSignalHandler() + + entryLog.Info("Starting ClusterProfiles provider example", "namespace", clusterInventoryNamespace) + + // Load credential providers from configuration file + cpCreds, err := credentials.NewFromFile(*credentialsProviders) + if err != nil { + log.Fatalf("Got error reading credentials providers: %v", err) + } + + // Create the clusterprofiles provider with options + providerOpts := clusterprofileprovider.Options{ + Namespace: clusterInventoryNamespace, + CredentialsProvider: cpCreds, + Scheme: scheme.Scheme, + } + + // Create the provider instance + provider := clusterprofileprovider.New(providerOpts) + + // Setup a cluster-aware Manager with the provider to lookup clusters + managerOpts := manager.Options{ + Metrics: metricsserver.Options{ + BindAddress: "0", // Disable metrics server + }, + } + + // Create multicluster manager that will coordinate across all discovered clusters + entryLog.Info("Creating multicluster manager") + mgr, err := mcmanager.New(ctrl.GetConfigOrDie(), provider, managerOpts) + if err != nil { + entryLog.Error(err, "Unable to create manager") + os.Exit(1) + } + + // Setup provider controller with the manager to start watching ClusterProfile resources + err = provider.SetupWithManager(ctx, mgr) + if err != nil { + entryLog.Error(err, "Unable to setup provider with manager") + os.Exit(1) + } + + // Create a multi-cluster controller that watches ConfigMaps across all connected clusters + // This demonstrates how to build controllers that operate across multiple clusters + err = mcbuilder.ControllerManagedBy(mgr). + 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") + + // Get the cluster client for the specific cluster where this resource lives + cl, err := mgr.GetCluster(ctx, req.ClusterName) + if err != nil { + return reconcile.Result{}, err + } + + // Retrieve the ConfigMap from the specific cluster + 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 found", "namespace", cm.Namespace, "name", cm.Name, "cluster", req.ClusterName) + + // Here you would add your multi-cluster reconciliation logic + // For example: + // - Sync resources across clusters + // - Aggregate data from multiple clusters + // - Implement cross-cluster policies + + return ctrl.Result{}, nil + }, + )) + if err != nil { + entryLog.Error(err, "unable to create controller") + os.Exit(1) + } + + // Start the manager - this will begin watching ClusterProfile resources + // and automatically connect to discovered clusters + entryLog.Info("Starting manager") + err = mgr.Start(ctx) + if err != nil && !errors.Is(err, context.Canceled) { + entryLog.Error(err, "unable to start") + os.Exit(1) + } +} diff --git a/providers/clusterprofile/clusterprofile_suite_test.go b/providers/clusterprofile/clusterprofile_suite_test.go new file mode 100644 index 0000000..fc3f699 --- /dev/null +++ b/providers/clusterprofile/clusterprofile_suite_test.go @@ -0,0 +1,112 @@ +/* +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 clusterprofile + +import ( + "testing" + + "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" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestBuilder(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "ClusterProfiles Provider Suite") +} + +var localEnv *envtest.Environment +var localCfg *rest.Config + +var zooEnv *envtest.Environment +var zooCfg *rest.Config + +var jungleEnv *envtest.Environment +var jungleCfg *rest.Config + +var islandEnv *envtest.Environment +var islandCfg *rest.Config + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + var err error + + // 'local' cluster runs the manager + localEnv = &envtest.Environment{ + UseExistingCluster: &[]bool{true}[0], + } + localCfg, err = localEnv.Start() + if err != nil { + Skip("Skipping tests due to missing kubebuilder/envtest setup: " + err.Error()) + } + + // startup 'remote' clusters + zooEnv = &envtest.Environment{ + UseExistingCluster: &[]bool{true}[0], + } + zooCfg, err = zooEnv.Start() + if err != nil { + Skip("Skipping tests due to missing kubebuilder/envtest setup: " + err.Error()) + } + + jungleEnv = &envtest.Environment{ + UseExistingCluster: &[]bool{true}[0], + } + jungleCfg, err = jungleEnv.Start() + if err != nil { + Skip("Skipping tests due to missing kubebuilder/envtest setup: " + err.Error()) + } + + islandEnv = &envtest.Environment{ + UseExistingCluster: &[]bool{true}[0], + } + islandCfg, err = islandEnv.Start() + if err != nil { + Skip("Skipping tests due to missing kubebuilder/envtest setup: " + err.Error()) + } + + // Prevent the metrics listener being created + metricsserver.DefaultBindAddress = "0" +}) + +var _ = AfterSuite(func() { + if localEnv != nil { + Expect(localEnv.Stop()).To(Succeed()) + } + + if zooEnv != nil { + Expect(zooEnv.Stop()).To(Succeed()) + } + + if jungleEnv != nil { + Expect(jungleEnv.Stop()).To(Succeed()) + } + + if islandEnv != nil { + Expect(islandEnv.Stop()).To(Succeed()) + } + + // Put the DefaultBindAddress back + metricsserver.DefaultBindAddress = ":8080" +}) diff --git a/providers/clusterprofile/provider.go b/providers/clusterprofile/provider.go new file mode 100644 index 0000000..b5cb802 --- /dev/null +++ b/providers/clusterprofile/provider.go @@ -0,0 +1,358 @@ +/* +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 clusterprofile provides a Kubernetes cluster provider that watches ClusterProfile +// resources and creates controller-runtime clusters for each using credential providers. +package clusterprofile + +import ( + "context" + "encoding/json" + "fmt" + "hash/fnv" + "sync" + + "github.com/go-logr/logr" + clusterinventory "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1" + "sigs.k8s.io/cluster-inventory-api/pkg/credentials" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + + ctrl "sigs.k8s.io/controller-runtime" + "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/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" + "sigs.k8s.io/multicluster-runtime/pkg/multicluster" +) + +var _ multicluster.Provider = &Provider{} + +// New creates a new ClusterProfile provider. +func New(opts Options) *Provider { + if opts.Scheme == nil { + opts.Scheme = scheme.Scheme + } + + if opts.CredentialsProvider == nil { + opts.CredentialsProvider = credentials.New([]credentials.Provider{}) + } + + return &Provider{ + opts: opts, + log: log.Log.WithName("clusterprofile-provider"), + clusters: map[string]activeCluster{}, + } +} + +// Options contains the configuration for the clusterprofile provider. +type Options struct { + // Namespace is the cluster inventory namespace. + Namespace string + // Scheme is the scheme to use for the clusters. + Scheme *runtime.Scheme + // CredentialsProvider is the credentials provider for the cluster profiles. + CredentialsProvider *credentials.CredentialsProvider +} + +type index struct { + object client.Object + field string + extractValue client.IndexerFunc +} + +// Provider is a cluster provider that watches for ClusterProfile resources +// and engages clusters based on their credential providers. +type Provider struct { + opts Options + log logr.Logger + lock sync.RWMutex // protects clusters and indexers + clusters map[string]activeCluster + indexers []index + mgr mcmanager.Manager +} + +type activeCluster struct { + Cluster cluster.Cluster + Context context.Context + Cancel context.CancelFunc + Hash string +} + +func (p *Provider) getCluster(clusterName string) (activeCluster, bool) { + p.lock.RLock() + defer p.lock.RUnlock() + + ac, exists := p.clusters[clusterName] + return ac, exists +} + +func (p *Provider) setCluster(clusterName string, ac activeCluster) { + p.lock.Lock() + defer p.lock.Unlock() + + p.clusters[clusterName] = ac +} + +func (p *Provider) addIndexer(idx index) { + p.lock.Lock() + defer p.lock.Unlock() + + p.indexers = append(p.indexers, idx) +} + +// Get returns the cluster with the given name, if it is known. +func (p *Provider) Get(ctx context.Context, clusterName string) (cluster.Cluster, error) { + ac, exists := p.getCluster(clusterName) + if !exists { + return nil, multicluster.ErrClusterNotFound + } + return ac.Cluster, nil +} + +// SetupWithManager sets up the provider with the manager. +func (p *Provider) SetupWithManager(ctx context.Context, mgr mcmanager.Manager) error { + log := p.log + log.Info("Starting clusterprofile provider", "options", p.opts) + + if mgr == nil { + return fmt.Errorf("manager is nil") + } + p.mgr = mgr + + // Get the local manager from the multicluster manager + localMgr := mgr.GetLocalManager() + if localMgr == nil { + return fmt.Errorf("local manager is nil") + } + + // Setup the controller to watch for cluster profiles. + err := ctrl.NewControllerManagedBy(localMgr). + For(&clusterinventory.ClusterProfile{}, builder.WithPredicates(predicate.NewPredicateFuncs( + func(obj client.Object) bool { + if p.opts.Namespace == "" { + return true + } + return obj.GetNamespace() == p.opts.Namespace + }, + ))). + Complete(p) + if err != nil { + return fmt.Errorf("failed to create controller: %w", err) + } + + return nil +} + +// Reconcile is the main controller function that reconciles cluster profiles +func (p *Provider) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + cp, err := p.getClusterProfile(ctx, req.NamespacedName) + if err != nil { + return ctrl.Result{}, err + } + + if cp == nil || cp.DeletionTimestamp != nil { + p.removeCluster(req.Name) + return ctrl.Result{}, nil + } + + clusterName := req.NamespacedName.String() + log := p.log.WithValues("cluster", clusterName) + + hashStr, err := p.hashClusterProfile(cp) + if err != nil { + return ctrl.Result{}, err + } + + // Check if cluster exists and needs to be updated + existingCluster, clusterExists := p.getCluster(clusterName) + if clusterExists { + if existingCluster.Hash == hashStr { + log.Info("Cluster already exists and has the hash, skipping") + return ctrl.Result{}, nil + } + // If the cluster exists and the configuration has changed, + // remove it and continue to create a new cluster in its place. + // Creating a new cluster will ensure all new configuration is applied. + log.Info("Cluster already exists, updating it") + p.removeCluster(clusterName) + } + + restConfig, err := p.opts.CredentialsProvider.BuildConfigFromCP(cp) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed to get REST config for cluster %q: %w", clusterName, err) + } + + // Create and setup the new cluster + if err := p.createAndEngageCluster(ctx, clusterName, restConfig, hashStr, log); err != nil { + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil +} + +// getClusterProfile retrieves a cluster profile and handles not found errors +func (p *Provider) getClusterProfile(ctx context.Context, namespacedName client.ObjectKey) (*clusterinventory.ClusterProfile, error) { + cp := &clusterinventory.ClusterProfile{} + if err := p.mgr.GetLocalManager().GetClient().Get(ctx, namespacedName, cp); err != nil { + if apierrors.IsNotFound(err) { + return nil, nil + } + return nil, fmt.Errorf("failed to get cluster profile: %w", err) + } + return cp, nil +} + +func (p *Provider) hashClusterProfile(cp *clusterinventory.ClusterProfile) (string, error) { + hash := fnv.New32a() + cpJSON, err := json.Marshal(cp) + if err != nil { + return "", fmt.Errorf("failed to marshal cluster profile: %w", err) + } + hash.Write(cpJSON) + return fmt.Sprintf("%x", hash.Sum32()), nil +} + +// createAndEngageCluster creates a new cluster, sets it up, stores it, and engages it with the manager +func (p *Provider) createAndEngageCluster(ctx context.Context, clusterName string, restConfig *rest.Config, hashStr string, log logr.Logger) error { + // Create a new cluster + log.Info("Creating new cluster from REST config") + cl, err := cluster.New(restConfig, func(o *cluster.Options) { + o.Scheme = p.opts.Scheme + }) + if err != nil { + return fmt.Errorf("failed to create cluster: %w", err) + } + + // Apply field indexers + if err := p.applyIndexers(ctx, cl); err != nil { + return err + } + + // Create a context that will be canceled when this cluster is removed + clusterCtx, cancel := context.WithCancel(ctx) + + // Start the cluster + go func() { + if err := cl.Start(clusterCtx); err != nil { + log.Error(err, "Failed to start cluster") + } + }() + + // Wait for cache to be ready + log.Info("Waiting for cluster cache to be ready") + if !cl.GetCache().WaitForCacheSync(clusterCtx) { + cancel() + return fmt.Errorf("failed to wait for cache sync") + } + log.Info("Cluster cache is ready") + + // Store the cluster + p.setCluster(clusterName, activeCluster{ + Cluster: cl, + Context: clusterCtx, + Cancel: cancel, + Hash: hashStr, + }) + + log.Info("Successfully added cluster") + + // Engage cluster so that the manager can start operating on the cluster + if err := p.mgr.Engage(clusterCtx, clusterName, cl); err != nil { + log.Error(err, "Failed to engage manager, removing cluster") + p.removeCluster(clusterName) + return fmt.Errorf("failed to engage manager: %w", err) + } + + log.Info("Successfully engaged manager") + return nil +} + +// applyIndexers applies field indexers to a cluster +func (p *Provider) applyIndexers(ctx context.Context, cl cluster.Cluster) error { + p.lock.RLock() + defer p.lock.RUnlock() + + for _, idx := range p.indexers { + if err := cl.GetFieldIndexer().IndexField(ctx, idx.object, idx.field, idx.extractValue); err != nil { + return fmt.Errorf("failed to index field %q: %w", idx.field, err) + } + } + + return 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 { + // Save for future clusters + p.addIndexer(index{ + object: obj, + field: field, + extractValue: extractValue, + }) + + // Apply to existing clusters + p.lock.RLock() + defer p.lock.RUnlock() + + for name, ac := range p.clusters { + if err := ac.Cluster.GetFieldIndexer().IndexField(ctx, obj, field, extractValue); err != nil { + return fmt.Errorf("failed to index field %q on cluster %q: %w", field, name, err) + } + } + + return nil +} + +// ListClusters returns a list of all discovered clusters. +func (p *Provider) ListClusters() []string { + p.lock.RLock() + defer p.lock.RUnlock() + + result := make([]string, 0, len(p.clusters)) + for name := range p.clusters { + result = append(result, name) + } + return result +} + +// removeCluster removes a cluster by name with write lock and cleanup +func (p *Provider) removeCluster(clusterName string) { + log := p.log.WithValues("cluster", clusterName) + + p.lock.Lock() + ac, exists := p.clusters[clusterName] + if !exists { + p.lock.Unlock() + log.Info("Cluster not found, nothing to remove") + return + } + + log.Info("Removing cluster") + delete(p.clusters, clusterName) + p.lock.Unlock() + + // Cancel the context to trigger cleanup for this cluster. + // This is done outside the lock to avoid holding the lock for a long time. + ac.Cancel() + log.Info("Successfully removed cluster and cancelled cluster context") +} diff --git a/providers/clusterprofile/provider_test.go b/providers/clusterprofile/provider_test.go new file mode 100644 index 0000000..0c0de48 --- /dev/null +++ b/providers/clusterprofile/provider_test.go @@ -0,0 +1,431 @@ +/* +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 clusterprofile + +import ( + "context" + "errors" + "fmt" + "strconv" + "sync" + "time" + + clusterinventory "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1" + "sigs.k8s.io/cluster-inventory-api/pkg/credentials" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/client-go/rest" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + clientcmdv1 "k8s.io/client-go/tools/clientcmd/api/v1" + "k8s.io/client-go/util/retry" + + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/cluster" + "sigs.k8s.io/controller-runtime/pkg/log" + 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" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +const clusterInventoryNamespace = "testing" + +var _ = Describe("Provider Namespace", Ordered, func() { + ctx, cancel := context.WithCancel(context.Background()) + wg := sync.WaitGroup{} + + var provider *Provider + var mgr mcmanager.Manager + var localCli client.Client + var zooCli client.Client + var jungleCli client.Client + var islandCli client.Client + + BeforeAll(func() { + var err error + localCli, err = client.New(localCfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + zooCli, err = client.New(zooCfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + jungleCli, err = client.New(jungleCfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + islandCli, err = client.New(islandCfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + + provider = New(Options{ + Namespace: clusterInventoryNamespace, + CredentialsProvider: credentials.New([]credentials.Provider{ + { + Name: "foobar", + ExecConfig: &clientcmdapi.ExecConfig{ + Command: "test-command-1", + Args: []string{"arg1", "arg2"}, + APIVersion: "client.authentication.k8s.io/v1beta1", + }, + }, + }), + }) + + By("Creating a namespace in the local cluster", func() { + namespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterInventoryNamespace, + }, + } + + err = localCli.Create(ctx, namespace) + Expect(err).NotTo(HaveOccurred()) + }) + + By("Creating cluster profiles in the local cluster", func() { + err = createClusterProfile(ctx, "zoo", zooCfg, localCli) + Expect(err).NotTo(HaveOccurred()) + + err = createClusterProfile(ctx, "jungle", jungleCfg, localCli) + Expect(err).NotTo(HaveOccurred()) + + err = createClusterProfile(ctx, "island", islandCfg, localCli) + Expect(err).NotTo(HaveOccurred()) + }) + + By("Setting up the cluster-aware manager, with the provider to lookup clusters", func() { + var err error + mgr, err = mcmanager.New(localCfg, provider, mcmanager.Options{ + Metrics: metricsserver.Options{ + BindAddress: "0", + }, + }) + Expect(err).NotTo(HaveOccurred()) + }) + + By("Setting up the provider with the manager", func() { + err := provider.SetupWithManager(ctx, mgr) + Expect(err).NotTo(HaveOccurred()) + }) + + By("Setting up the controller feeding the animals", func() { + err := mcbuilder.ControllerManagedBy(mgr). + Named("fleet-ns-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) + } + + 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() { + wg.Add(1) + go func() { + err := ignoreCanceled(mgr.Start(ctx)) + Expect(err).NotTo(HaveOccurred()) + wg.Done() + }() + }) + + }) + + BeforeAll(func() { + utilruntime.Must(client.IgnoreAlreadyExists(zooCli.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "zoo"}}))) + utilruntime.Must(client.IgnoreAlreadyExists(zooCli.Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: "zoo", Name: "elephant", Labels: map[string]string{"type": "animal"}}}))) + utilruntime.Must(client.IgnoreAlreadyExists(zooCli.Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: "zoo", Name: "lion", Labels: map[string]string{"type": "animal"}}}))) + utilruntime.Must(client.IgnoreAlreadyExists(zooCli.Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: "zoo", Name: "keeper", Labels: map[string]string{"type": "human"}}}))) + + utilruntime.Must(client.IgnoreAlreadyExists(jungleCli.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "jungle"}}))) + utilruntime.Must(client.IgnoreAlreadyExists(jungleCli.Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: "jungle", Name: "monkey", Labels: map[string]string{"type": "animal"}}}))) + utilruntime.Must(client.IgnoreAlreadyExists(jungleCli.Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: "jungle", Name: "tree", Labels: map[string]string{"type": "thing"}}}))) + utilruntime.Must(client.IgnoreAlreadyExists(jungleCli.Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: "jungle", Name: "tarzan", Labels: map[string]string{"type": "human"}}}))) + + utilruntime.Must(client.IgnoreAlreadyExists(islandCli.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "island"}}))) + utilruntime.Must(client.IgnoreAlreadyExists(islandCli.Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: "island", Name: "bird", Labels: map[string]string{"type": "animal"}}}))) + utilruntime.Must(client.IgnoreAlreadyExists(islandCli.Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: "island", Name: "stone", Labels: map[string]string{"type": "thing"}}}))) + utilruntime.Must(client.IgnoreAlreadyExists(islandCli.Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: "island", Name: "crusoe", Labels: map[string]string{"type": "human"}}}))) + }) + + It("lists the clusters loaded from cluster profiles", func() { + Eventually(provider.ListClusters, "10s").Should(HaveLen(3)) + }) + + It("runs the reconciler for existing objects", func(ctx context.Context) { + Eventually(func() string { + lion := &corev1.ConfigMap{} + err := zooCli.Get(ctx, client.ObjectKey{Namespace: "zoo", Name: "lion"}, 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 := zooCli.Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: "zoo", Name: "tiger", Labels: map[string]string{"type": "animal"}}}) + Expect(err).NotTo(HaveOccurred()) + }) + + Eventually(func() string { + tiger := &corev1.ConfigMap{} + err := zooCli.Get(ctx, client.ObjectKey{Namespace: "zoo", Name: "tiger"}, 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 elephant's stomach", func() { + err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + if err := zooCli.Get(ctx, client.ObjectKey{Namespace: "zoo", Name: "elephant"}, updated); err != nil { + return err + } + updated.Data = map[string]string{} + return zooCli.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 := zooCli.Get(ctx, client.ObjectKey{Namespace: "zoo", Name: "elephant"}, 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 := zooCli.Get(ctx, client.ObjectKey{Namespace: "zoo", Name: "elephant"}, elephant) + Expect(err).NotTo(HaveOccurred()) + return elephant.Data["stomach"] + }, "10s").Should(Equal("food")) + }) + + It("queries one cluster via a multi-cluster index", func() { + island, err := mgr.GetCluster(ctx, "island") + Expect(err).NotTo(HaveOccurred()) + + cms := &corev1.ConfigMapList{} + err = island.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("crusoe")) + Expect(cms.Items[0].Namespace).To(Equal("island")) + }) + + It("reconciles objects when the cluster is updated in cluster profile", func() { + islandClusterProfile := &clusterinventory.ClusterProfile{} + err := localCli.Get(ctx, client.ObjectKey{Name: "island", Namespace: clusterInventoryNamespace}, islandClusterProfile) + Expect(err).NotTo(HaveOccurred()) + + // Update the cluster profile to point to jungle configuration + islandClusterProfile.Status.CredentialProviders = []clusterinventory.CredentialProvider{ + { + Name: "foobar", + Cluster: clientcmdv1.Cluster{ + Server: jungleCfg.Host, + CertificateAuthorityData: jungleCfg.CAData, + }, + }, + } + err = localCli.Status().Update(ctx, islandClusterProfile) + Expect(err).NotTo(HaveOccurred()) + + err = jungleCli.Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: "jungle", Name: "dog", Labels: map[string]string{"type": "animal"}}}) + Expect(err).NotTo(HaveOccurred()) + + Eventually(func() string { + dog := &corev1.ConfigMap{} + err := jungleCli.Get(ctx, client.ObjectKey{Namespace: "jungle", Name: "dog"}, dog) + Expect(err).NotTo(HaveOccurred()) + return dog.Data["stomach"] + }, "10s").Should(Equal("food")) + }) + + It("reconciles objects when cluster profile is updated without changing the cluster", func() { + jungleClusterProfile := &clusterinventory.ClusterProfile{} + err := localCli.Get(ctx, client.ObjectKey{Name: "jungle", Namespace: clusterInventoryNamespace}, jungleClusterProfile) + Expect(err).NotTo(HaveOccurred()) + + jungleClusterProfile.ObjectMeta.Annotations = map[string]string{ + "location": "amazon", + } + err = localCli.Update(ctx, jungleClusterProfile) + Expect(err).NotTo(HaveOccurred()) + + err = jungleCli.Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: "jungle", Name: "leopard", Labels: map[string]string{"type": "animal"}}}) + Expect(err).NotTo(HaveOccurred()) + + Eventually(func() string { + leopard := &corev1.ConfigMap{} + err := jungleCli.Get(ctx, client.ObjectKey{Namespace: "jungle", Name: "leopard"}, leopard) + Expect(err).NotTo(HaveOccurred()) + return leopard.Data["stomach"] + }, "10s").Should(Equal("food")) + }) + + It("removes a cluster from the provider when the cluster profile is deleted", func() { + err := localCli.Delete(ctx, &clusterinventory.ClusterProfile{ObjectMeta: metav1.ObjectMeta{Name: "island", Namespace: clusterInventoryNamespace}}) + Expect(err).NotTo(HaveOccurred()) + Eventually(provider.ListClusters, "10s").Should(HaveLen(2)) + }) + + AfterAll(func() { + By("Stopping the provider, cluster, manager, and controller", func() { + cancel() + wg.Wait() + }) + }) +}) + +var _ = Describe("Provider race condition", func() { + It("should handle concurrent operations without issues", func() { + p := New(Options{}) + + // Pre-populate with some clusters to make the test meaningful + numClusters := 20 + for i := 0; i < numClusters; i++ { + clusterName := fmt.Sprintf("cluster-%d", i) + p.clusters[clusterName] = activeCluster{ + Cluster: &mockCluster{}, + Cancel: func() {}, + } + } + + var wg sync.WaitGroup + numGoroutines := 40 + wg.Add(numGoroutines) + + for i := 0; i < numGoroutines; i++ { + go func(i int) { + defer GinkgoRecover() + defer wg.Done() + + // Mix of operations to stress the provider + switch i % 4 { + case 0: + // Concurrently index a field. This will read the cluster list. + err := p.IndexField(context.Background(), &corev1.Pod{}, "spec.nodeName", func(rawObj client.Object) []string { + return nil + }) + Expect(err).NotTo(HaveOccurred()) + case 1: + // Concurrently get a cluster. + _, err := p.Get(context.Background(), "cluster-1") + Expect(err).To(Or(BeNil(), MatchError("cluster cluster-1 not found"))) + case 2: + // Concurrently list clusters. + p.ListClusters() + case 3: + // Concurrently delete a cluster. This will modify the cluster map. + clusterToRemove := fmt.Sprintf("cluster-%d", i/4) + p.removeCluster(clusterToRemove) + } + }(i) + } + + wg.Wait() + }) +}) + +func ignoreCanceled(err error) error { + if errors.Is(err, context.Canceled) { + return nil + } + return err +} + +func createClusterProfile(ctx context.Context, name string, cfg *rest.Config, cl client.Client) error { + cp := &clusterinventory.ClusterProfile{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: clusterInventoryNamespace, + }, + Status: clusterinventory.ClusterProfileStatus{ + CredentialProviders: []clusterinventory.CredentialProvider{ + { + Name: "foobar", + Cluster: clientcmdv1.Cluster{ + Server: cfg.Host, + CertificateAuthorityData: cfg.CAData, + }, + }, + }, + }, + } + + return cl.Create(ctx, cp) +} + +// mockCluster is a mock implementation of cluster.Cluster for testing. +type mockCluster struct { + cluster.Cluster +} + +func (c *mockCluster) GetFieldIndexer() client.FieldIndexer { + return &mockFieldIndexer{} +} + +type mockFieldIndexer struct{} + +func (f *mockFieldIndexer) IndexField(ctx context.Context, obj client.Object, field string, extractValue client.IndexerFunc) error { + // Simulate work to increase chance of race + time.Sleep(time.Millisecond) + return nil +} From 1ed94001ac850217da2ce33500c6b4fc09a68a37 Mon Sep 17 00:00:00 2001 From: Shingo Omura Date: Sat, 26 Jul 2025 02:32:09 +0900 Subject: [PATCH 14/18] Unify ClusterProfiles provider implementation into cluster-inventory-api provider Signed-off-by: Shingo Omura --- .../clusterprofile-provider-file.json | 0 examples/cluster-inventory-api/go.sum | 5 + examples/cluster-inventory-api/main.go | 27 +- examples/clusterprofile/go.mod | 73 --- examples/clusterprofile/go.sum | 196 ------- examples/clusterprofile/main.go | 176 ------ providers/cluster-inventory-api/go.sum | 6 + .../kubeconfigstrategy/credentialsprovider.go | 42 ++ .../kubeconfigstrategy/factory.go | 27 +- .../kubeconfigstrategy/secret.go | 5 +- providers/cluster-inventory-api/provider.go | 6 +- .../cluster-inventory-api/provider_test.go | 547 ++++++++++++------ providers/cluster-inventory-api/suite_test.go | 29 +- .../clusterprofile_suite_test.go | 112 ---- providers/clusterprofile/provider.go | 358 ------------ providers/clusterprofile/provider_test.go | 431 -------------- 16 files changed, 459 insertions(+), 1581 deletions(-) rename examples/{clusterprofile => cluster-inventory-api}/clusterprofile-provider-file.json (100%) delete mode 100644 examples/clusterprofile/go.mod delete mode 100644 examples/clusterprofile/go.sum delete mode 100644 examples/clusterprofile/main.go create mode 100644 providers/cluster-inventory-api/kubeconfigstrategy/credentialsprovider.go delete mode 100644 providers/clusterprofile/clusterprofile_suite_test.go delete mode 100644 providers/clusterprofile/provider.go delete mode 100644 providers/clusterprofile/provider_test.go diff --git a/examples/clusterprofile/clusterprofile-provider-file.json b/examples/cluster-inventory-api/clusterprofile-provider-file.json similarity index 100% rename from examples/clusterprofile/clusterprofile-provider-file.json rename to examples/cluster-inventory-api/clusterprofile-provider-file.json diff --git a/examples/cluster-inventory-api/go.sum b/examples/cluster-inventory-api/go.sum index ae644f6..32d2ae6 100644 --- a/examples/cluster-inventory-api/go.sum +++ b/examples/cluster-inventory-api/go.sum @@ -69,7 +69,10 @@ 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/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 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= @@ -158,6 +161,8 @@ gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSP 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/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 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= diff --git a/examples/cluster-inventory-api/main.go b/examples/cluster-inventory-api/main.go index dcbf60b..af357af 100644 --- a/examples/cluster-inventory-api/main.go +++ b/examples/cluster-inventory-api/main.go @@ -19,10 +19,12 @@ package main import ( "context" "errors" + "flag" "os" "golang.org/x/sync/errgroup" clusterinventoryv1alpha1 "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1" + "sigs.k8s.io/cluster-inventory-api/pkg/credentials" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -50,6 +52,9 @@ func init() { } func main() { + credentialsProviderFile := credentials.SetupProviderFileFlag() + flag.Parse() + ctrllog.SetLogger(zap.New(zap.UseDevMode(true))) entryLog := ctrllog.Log.WithName("entrypoint") ctx := signals.SetupSignalHandler() @@ -61,13 +66,25 @@ func main() { os.Exit(1) } - // Create the provider against the local manager. + // Load credential providers from configuration file + credentialsProvider, err := credentials.NewFromFile(*credentialsProviderFile) + if err != nil { + entryLog.Error(err, "Got error reading credentials providers") + os.Exit(1) + } + + // Create the provider. provider, err := clusterinventoryapi.New(clusterinventoryapi.Options{ + // Specifying the strategy how to fetch kubeconfig from ClusterProfile. KubeconfigStrategyOption: kubeconfigstrategy.Option{ - // Use the Secret strategy with a specific consumer name. - Secret: kubeconfigstrategy.SecretStrategyOption{ - ConsumerName: "cluster-inventory-api-consumer", + CredentialsProvider: &kubeconfigstrategy.CredentialsProviderOption{ + Provider: credentialsProvider, }, + // // Alternative: + // // You can use the Secret strategy, but it is not recommended for production use. + // Secret: &kubeconfigstrategy.SecretStrategyOption{ + // ConsumerName: "cluster-inventory-api-consumer", + // }, }, }) if err != nil { @@ -76,7 +93,7 @@ func main() { } // Create a multi-cluster manager attached to the provider. - entryLog.Info("Setting up local manager") + entryLog.Info("Setting up manager") mcMgr, err := mcmanager.New(cfg, provider, manager.Options{ LeaderElection: false, Metrics: metricsserver.Options{ diff --git a/examples/clusterprofile/go.mod b/examples/clusterprofile/go.mod deleted file mode 100644 index 9689e71..0000000 --- a/examples/clusterprofile/go.mod +++ /dev/null @@ -1,73 +0,0 @@ -module sigs.k8s.io/multicluster-runtime/examples/clusterprofile - -go 1.24.0 - -replace ( - sigs.k8s.io/multicluster-runtime => ../.. - sigs.k8s.io/multicluster-runtime/providers/clusterprofile => ../../providers/clusterprofile -) - -require ( - k8s.io/api v0.33.3 - k8s.io/apimachinery v0.33.3 - k8s.io/kubectl v0.33.3 - sigs.k8s.io/cluster-inventory-api v0.0.0-20250702132726-0f613c6275a5 - sigs.k8s.io/controller-runtime v0.21.0 - sigs.k8s.io/multicluster-runtime 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.8.0 // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect - github.com/go-logr/logr v1.4.3 // 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.6 // 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.39.0 // indirect - golang.org/x/oauth2 v0.29.0 // indirect - golang.org/x/sync v0.15.0 // indirect - golang.org/x/sys v0.32.0 // indirect - golang.org/x/term v0.31.0 // indirect - golang.org/x/text v0.24.0 // indirect - golang.org/x/time v0.11.0 // indirect - gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/protobuf v1.36.6 // 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/client-go v0.33.3 // 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/clusterprofile/go.sum b/examples/clusterprofile/go.sum deleted file mode 100644 index 57e84b0..0000000 --- a/examples/clusterprofile/go.sum +++ /dev/null @@ -1,196 +0,0 @@ -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.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= -github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -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.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/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/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -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.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/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.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= -golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= -golang.org/x/oauth2 v0.29.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.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= -golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= -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.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= -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.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= -golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= -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.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= -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/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -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.3 h1:SRd5t//hhkI1buzxb288fy2xvjubstenEKL9K51KBI8= -k8s.io/api v0.33.3/go.mod h1:01Y/iLUjNBM3TAvypct7DIj0M0NIZc+PzAHCIo0CYGE= -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.3 h1:4ZSrmNa0c/ZpZJhAgRdcsFcZOw1PQU1bALVQ0B3I5LA= -k8s.io/apimachinery v0.33.3/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/client-go v0.33.3 h1:M5AfDnKfYmVJif92ngN532gFqakcGi6RvaOF16efrpA= -k8s.io/client-go v0.33.3/go.mod h1:luqKBQggEf3shbxHY4uVENAxrDISLOarxpTKMiUuujg= -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/kubectl v0.33.3 h1:r/phHvH1iU7gO/l7tTjQk2K01ER7/OAJi8uFHHyWSac= -k8s.io/kubectl v0.33.3/go.mod h1:euj2bG56L6kUGOE/ckZbCoudPwuj4Kud7BR0GzyNiT0= -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-20250702132726-0f613c6275a5 h1:gonrKU7V8WnTnsGs/QasvvgWbS+S5GwXWfuKv4357HM= -sigs.k8s.io/cluster-inventory-api v0.0.0-20250702132726-0f613c6275a5/go.mod h1:uHmRJn/DNc0ScvENOAIagQPfXdjJfuDB7IgZxCpSLMM= -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/clusterprofile/main.go b/examples/clusterprofile/main.go deleted file mode 100644 index 7d11a55..0000000 --- a/examples/clusterprofile/main.go +++ /dev/null @@ -1,176 +0,0 @@ -/* -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. -*/ - -// This example demonstrates how to use the ClusterProfiles provider to create -// a multi-cluster operator that can watch and reconcile resources across -// multiple Kubernetes clusters using ClusterProfile resources. -// -// The operator watches for ClusterProfile resources in a specified namespace -// and automatically creates connections to the clusters they reference using -// configured credential providers. -// -// Usage: -// -// go run main.go --namespace cluster-inventory --credential-providers-file clusterprofile-provider-file.json -// -// This will: -// 1. Set up the ClusterProfiles provider with credential providers -// 2. Watch for ClusterProfile resources in the specified namespace -// 3. Create a multi-cluster controller that watches ConfigMaps across all connected clusters -// 4. Log information about discovered ConfigMaps -package main - -import ( - "context" - "errors" - "flag" - "log" - "os" - - "k8s.io/apimachinery/pkg/util/runtime" - "k8s.io/client-go/kubernetes/scheme" - "sigs.k8s.io/cluster-inventory-api/pkg/credentials" - "sigs.k8s.io/controller-runtime/pkg/log/zap" - "sigs.k8s.io/controller-runtime/pkg/manager" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - corev1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - clusterinventory "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1" - ctrl "sigs.k8s.io/controller-runtime" - ctrllog "sigs.k8s.io/controller-runtime/pkg/log" - metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" - mcbuilder "sigs.k8s.io/multicluster-runtime/pkg/builder" - mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" - mcreconcile "sigs.k8s.io/multicluster-runtime/pkg/reconcile" - clusterprofileprovider "sigs.k8s.io/multicluster-runtime/providers/clusterprofile" -) - -func init() { - runtime.Must(clusterinventory.AddToScheme(scheme.Scheme)) -} - -func main() { - // Set up command line flags for credential providers configuration - credentialsProviders := credentials.SetupProviderFileFlag() - flag.Parse() - - // Define the namespace where ClusterProfile resources will be watched - var clusterInventoryNamespace string - flag.StringVar(&clusterInventoryNamespace, "namespace", "", "Cluster inventory namespace") - - // Set up logging options - opts := zap.Options{ - Development: true, - } - opts.BindFlags(flag.CommandLine) - flag.Parse() - - // Initialize logger and signal handler - ctrllog.SetLogger(zap.New(zap.UseFlagOptions(&opts))) - entryLog := ctrllog.Log.WithName("entrypoint") - ctx := ctrl.SetupSignalHandler() - - entryLog.Info("Starting ClusterProfiles provider example", "namespace", clusterInventoryNamespace) - - // Load credential providers from configuration file - cpCreds, err := credentials.NewFromFile(*credentialsProviders) - if err != nil { - log.Fatalf("Got error reading credentials providers: %v", err) - } - - // Create the clusterprofiles provider with options - providerOpts := clusterprofileprovider.Options{ - Namespace: clusterInventoryNamespace, - CredentialsProvider: cpCreds, - Scheme: scheme.Scheme, - } - - // Create the provider instance - provider := clusterprofileprovider.New(providerOpts) - - // Setup a cluster-aware Manager with the provider to lookup clusters - managerOpts := manager.Options{ - Metrics: metricsserver.Options{ - BindAddress: "0", // Disable metrics server - }, - } - - // Create multicluster manager that will coordinate across all discovered clusters - entryLog.Info("Creating multicluster manager") - mgr, err := mcmanager.New(ctrl.GetConfigOrDie(), provider, managerOpts) - if err != nil { - entryLog.Error(err, "Unable to create manager") - os.Exit(1) - } - - // Setup provider controller with the manager to start watching ClusterProfile resources - err = provider.SetupWithManager(ctx, mgr) - if err != nil { - entryLog.Error(err, "Unable to setup provider with manager") - os.Exit(1) - } - - // Create a multi-cluster controller that watches ConfigMaps across all connected clusters - // This demonstrates how to build controllers that operate across multiple clusters - err = mcbuilder.ControllerManagedBy(mgr). - 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") - - // Get the cluster client for the specific cluster where this resource lives - cl, err := mgr.GetCluster(ctx, req.ClusterName) - if err != nil { - return reconcile.Result{}, err - } - - // Retrieve the ConfigMap from the specific cluster - 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 found", "namespace", cm.Namespace, "name", cm.Name, "cluster", req.ClusterName) - - // Here you would add your multi-cluster reconciliation logic - // For example: - // - Sync resources across clusters - // - Aggregate data from multiple clusters - // - Implement cross-cluster policies - - return ctrl.Result{}, nil - }, - )) - if err != nil { - entryLog.Error(err, "unable to create controller") - os.Exit(1) - } - - // Start the manager - this will begin watching ClusterProfile resources - // and automatically connect to discovered clusters - entryLog.Info("Starting manager") - err = mgr.Start(ctx) - if err != nil && !errors.Is(err, context.Canceled) { - entryLog.Error(err, "unable to start") - os.Exit(1) - } -} diff --git a/providers/cluster-inventory-api/go.sum b/providers/cluster-inventory-api/go.sum index 15c1baa..f2dcdcc 100644 --- a/providers/cluster-inventory-api/go.sum +++ b/providers/cluster-inventory-api/go.sum @@ -69,6 +69,10 @@ 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/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 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= @@ -157,6 +161,8 @@ gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSP 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/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 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= diff --git a/providers/cluster-inventory-api/kubeconfigstrategy/credentialsprovider.go b/providers/cluster-inventory-api/kubeconfigstrategy/credentialsprovider.go new file mode 100644 index 0000000..0ff3762 --- /dev/null +++ b/providers/cluster-inventory-api/kubeconfigstrategy/credentialsprovider.go @@ -0,0 +1,42 @@ +package kubeconfigstrategy + +import ( + "context" + + "k8s.io/client-go/rest" + + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1" + "sigs.k8s.io/cluster-inventory-api/pkg/credentials" +) + +var _ Interface = &credentialsProviderStrategy{} + +// CredentialsProviderOption specifies the credentials provider option. +// It contains the credentials provider that will be used to build the kubeconfig. +type CredentialsProviderOption struct { + Provider *credentials.CredentialsProvider +} + +type credentialsProviderStrategy struct { + provider *credentials.CredentialsProvider +} + +func newCredentialsProviderStrategy(ctx context.Context, option CredentialsProviderOption) (Interface, error) { + log.FromContext(ctx).Info("Using CredentialsProvider strategy for fetching kubeconfig from ClusterProfile") + return &credentialsProviderStrategy{ + provider: option.Provider, + }, nil +} + +// CustomWatches implements Interface. +func (c *credentialsProviderStrategy) CustomWatches() []CustomWatch { + return nil +} + +// GetKubeConfig implements Interface. +func (c *credentialsProviderStrategy) GetKubeConfig(ctx context.Context, cli client.Client, clp *v1alpha1.ClusterProfile) (*rest.Config, error) { + return c.provider.BuildConfigFromCP(clp) +} diff --git a/providers/cluster-inventory-api/kubeconfigstrategy/factory.go b/providers/cluster-inventory-api/kubeconfigstrategy/factory.go index 15ae8ca..617154d 100644 --- a/providers/cluster-inventory-api/kubeconfigstrategy/factory.go +++ b/providers/cluster-inventory-api/kubeconfigstrategy/factory.go @@ -1,14 +1,31 @@ package kubeconfigstrategy -import "context" +import ( + "context" + "fmt" +) -// Option specifies which strategy will be applied +// Option specifies which strategy will be applied to fetch the kubeconfig from ClusterProfile. +// Either Secret or CredentialsProvider must be set, but not both. type Option struct { - // Secret specifies option for the kubeconfig strategy based on a Secret. - Secret SecretStrategyOption + // Secret specifies option for the Secret strategy + Secret *SecretStrategyOption + + // CredentialsProvider specifies option for CredentialsProvider strategy. + CredentialsProvider *CredentialsProviderOption } // New creates a new kubeconfig strategy based on the provided options. func New(ctx context.Context, option Option) (Interface, error) { - return newSecretKubeConfigStrategy(ctx, option.Secret) + if option.CredentialsProvider == nil && option.Secret == nil { + return nil, fmt.Errorf("either CredentialsProvider or Secret must be provided") + } + if option.CredentialsProvider != nil && option.Secret != nil { + return nil, fmt.Errorf("only one of CredentialsProvider or Secret can be provided") + } + + if option.Secret != nil { + return newSecretKubeConfigStrategy(ctx, *option.Secret) + } + return newCredentialsProviderStrategy(ctx, *option.CredentialsProvider) } diff --git a/providers/cluster-inventory-api/kubeconfigstrategy/secret.go b/providers/cluster-inventory-api/kubeconfigstrategy/secret.go index ce2d508..cd68346 100644 --- a/providers/cluster-inventory-api/kubeconfigstrategy/secret.go +++ b/providers/cluster-inventory-api/kubeconfigstrategy/secret.go @@ -6,18 +6,17 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" - "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1" - "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1" ) const ( diff --git a/providers/cluster-inventory-api/provider.go b/providers/cluster-inventory-api/provider.go index 5bc68b5..f8924a6 100644 --- a/providers/cluster-inventory-api/provider.go +++ b/providers/cluster-inventory-api/provider.go @@ -25,8 +25,6 @@ import ( "github.com/go-logr/logr" - clusterinventoryv1alpha1 "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1" - apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -42,12 +40,14 @@ import ( mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" "sigs.k8s.io/multicluster-runtime/pkg/multicluster" + clusterinventoryv1alpha1 "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1" + "sigs.k8s.io/multicluster-runtime/providers/cluster-inventory-api/kubeconfigstrategy" ) var _ multicluster.Provider = &Provider{} -// Options are the options for the Cluster-API cluster Provider. +// Options are the options for the Cluster Inventory API Provider. type Options struct { // ClusterOptions are the options passed to the cluster constructor. ClusterOptions []cluster.Option diff --git a/providers/cluster-inventory-api/provider_test.go b/providers/cluster-inventory-api/provider_test.go index 075c283..fb32356 100644 --- a/providers/cluster-inventory-api/provider_test.go +++ b/providers/cluster-inventory-api/provider_test.go @@ -31,67 +31,91 @@ import ( 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" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + clientcmdv1 "k8s.io/client-go/tools/clientcmd/api/v1" "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/config" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/envtest" "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" + mccontroller "sigs.k8s.io/multicluster-runtime/pkg/controller" mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" mcreconcile "sigs.k8s.io/multicluster-runtime/pkg/reconcile" + + clusterinventoryv1alpha1 "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1" + "sigs.k8s.io/cluster-inventory-api/pkg/credentials" + "sigs.k8s.io/multicluster-runtime/providers/cluster-inventory-api/kubeconfigstrategy" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) -var _ = Describe("Provider Cluster Inventory API With Secret Kubeconfig Strategy", Ordered, func() { - ctx, cancel := context.WithCancel(context.Background()) - g, ctx := errgroup.WithContext(ctx) - - const consumerName = "hub" - var provider *Provider - var mgr mcmanager.Manager +var _ = Describe("Provider Cluster Inventory API", Ordered, func() { + var ctx context.Context + var cancel context.CancelFunc + var g *errgroup.Group + var testenvHub *envtest.Environment + var cfgHub *rest.Config var cliHub client.Client + var testenvMember *envtest.Environment + var cfgMember *rest.Config var cliMember client.Client + + var provider *Provider + var mgr mcmanager.Manager var profileMember *clusterinventoryv1alpha1.ClusterProfile - var sa1TokenMember string - var sa2TokenMember string - BeforeAll(func() { + // + // Common Behaviors + // + createClusters := 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 Provider", func() { - provider, err = New(Options{ - KubeconfigStrategyOption: kubeconfigstrategy.Option{ - Secret: kubeconfigstrategy.SecretStrategyOption{ - ConsumerName: consumerName, - }, - }, - }) + By("Creating the hub cluster", func() { + testenvHub = &envtest.Environment{ + ErrorIfCRDPathMissing: true, + CRDDirectoryPaths: []string{clusterProfileCRDPath}, + } + cfgHub, err = testenvHub.Start() + Expect(err).NotTo(HaveOccurred()) + cliHub, err = client.New(cfgHub, client.Options{}) Expect(err).NotTo(HaveOccurred()) - Expect(provider).NotTo(BeNil()) }) - + By("Creating the member cluster", func() { + testenvMember = &envtest.Environment{} + cfgMember, err = testenvMember.Start() + Expect(err).NotTo(HaveOccurred()) + cliMember, err = client.New(cfgMember, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + }) + } + shutDownClusters := func() { + By("Stopping the hub cluster environment", func() { + Expect(testenvHub.Stop()).To(Succeed()) + }) + By("Stopping the member cluster environment", func() { + Expect(testenvMember.Stop()).To(Succeed()) + }) + } + setupAndStartControllers := func() { 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{}) + mgr, err = mcmanager.New(cfgHub, provider, manager.Options{ + Controller: config.Controller{ + SkipNameValidation: ptr.To(true), + }, + }) Expect(err).NotTo(HaveOccurred()) }) @@ -103,37 +127,38 @@ var _ = Describe("Provider Cluster Inventory API With Secret Kubeconfig Strategy By("Setting up the controller feeding the animals", func() { err := mcbuilder.ControllerManagedBy(mgr). Named("fleet-configmap-controller"). + WithOptions(mccontroller.Options{ + SkipNameValidation: ptr.To(true), + }). 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" { + 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 } - - 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 - }, - )) + 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() { @@ -145,169 +170,307 @@ var _ = Describe("Provider Cluster Inventory API With Secret Kubeconfig Strategy }) By("Starting the provider, cluster, manager, and controller", func() { - g.Go(func() error { err := mgr.Start(ctx) return ignoreCanceled(err) }) }) + } + createObjects := func() { + By("Creating the namespace and configmaps", func() { + Expect( + client.IgnoreAlreadyExists(cliMember.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "jungle"}})), + ).To(Succeed()) + Expect( + client.IgnoreAlreadyExists(cliMember.Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: "jungle", Name: "monkey", Labels: map[string]string{"type": "animal"}}})), + ).To(Succeed()) + Expect( + client.IgnoreAlreadyExists(cliMember.Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: "jungle", Name: "tree", Labels: map[string]string{"type": "thing"}}})), + ).To(Succeed()) + Expect( + client.IgnoreAlreadyExists(cliMember.Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: "jungle", Name: "tarzan", Labels: map[string]string{"type": "human"}}})), + ).To(Succeed()) + }) + } + assertBasicControllerBehavior := func() { + It("runs the reconciler for existing objects", func(ctx context.Context) { + Eventually(func() string { + monkey := &corev1.ConfigMap{} + err := cliMember.Get(ctx, client.ObjectKey{Namespace: "jungle", Name: "monkey"}, monkey) + Expect(err).NotTo(HaveOccurred()) + return monkey.Data["stomach"] + }, "10s").Should(Equal("food")) + }) - 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()) - // Mock the control plane health condition - 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(), + 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()) }) - Expect(cliHub.Status().Update(ctx, profileMember)).To(Succeed()) - - _, sa1TokenMember = mustCreateAdminSAAndToken(ctx, cliMember, "sa1", "default") - _ = mustCreateOrUpdateKubeConfigSecretFromTokenSecret( - ctx, cliHub, cfgMember, - consumerName, - *profileMember, - sa1TokenMember, - ) + + Eventually(func() string { + gorilla := &corev1.ConfigMap{} + err := cliMember.Get(ctx, client.ObjectKey{Namespace: "jungle", Name: "gorilla"}, gorilla) + Expect(err).NotTo(HaveOccurred()) + return gorilla.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()) - 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"}}}))) - }) + Eventually(func() int64 { + gorilla := &corev1.ConfigMap{} + err := cliMember.Get(ctx, client.ObjectKey{Namespace: "jungle", Name: "gorilla"}, gorilla) + Expect(err).NotTo(HaveOccurred()) + rv, err := strconv.ParseInt(gorilla.ResourceVersion, 10, 64) + Expect(err).NotTo(HaveOccurred()) + return rv + }, "10s").Should(BeNumerically(">=", rv)) - 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) + Eventually(func() string { + gorilla := &corev1.ConfigMap{} + err := cliMember.Get(ctx, client.ObjectKey{Namespace: "jungle", Name: "gorilla"}, gorilla) + Expect(err).NotTo(HaveOccurred()) + return gorilla.Data["stomach"] + }, "10s").Should(Equal("food")) + }) + } + assertClusterIndexBehavior := func() { + It("queries one cluster via a multi-cluster index", func() { + cl, err := mgr.GetCluster(ctx, "default/member") 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"}}}) + 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")) }) - Eventually(func() string { - tiger := &corev1.ConfigMap{} - err := cliMember.Get(ctx, client.ObjectKey{Namespace: "jungle", Name: "gorilla"}, tiger) + It("queries all clusters via a multi-cluster index with a namespace", func() { + cl, err := mgr.GetCluster(ctx, "default/member") 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) - }) + 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")) }) - 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)) + Context("With Secret-based kubeconfig strategy", Ordered, func() { + const consumerName = "hub" + var sa1TokenMember string + var sa2TokenMember string - 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")) - }) + BeforeAll(func() { + ctx, cancel = context.WithCancel(context.Background()) + g, _ = errgroup.WithContext(ctx) - It("queries one cluster via a multi-cluster index", func() { - cl, err := mgr.GetCluster(ctx, "default/member") - Expect(err).NotTo(HaveOccurred()) + createClusters() - 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")) - }) + By("Setting up the Provider", func() { + var err error + provider, err = New(Options{ + KubeconfigStrategyOption: kubeconfigstrategy.Option{ + Secret: &kubeconfigstrategy.SecretStrategyOption{ + ConsumerName: consumerName, + }, + }, + }) + Expect(err).NotTo(HaveOccurred()) + Expect(provider).NotTo(BeNil()) + }) - 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")) - }) + setupAndStartControllers() + + 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()) + // Mock the control plane health condition + 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, + ) + }) - 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, - ) + createObjects() }) - 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")) + assertBasicControllerBehavior() + assertClusterIndexBehavior() + + 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()) + }) + shutDownClusters() }) }) - AfterAll(func() { - By("Stopping the provider, cluster, manager, and controller", func() { - cancel() + Context("With Credential-based kubeconfig strategy", Ordered, func() { + var cancel context.CancelFunc + var g *errgroup.Group + credentialProviderName := "test" + + BeforeAll(func() { + ctx, cancel = context.WithCancel(context.Background()) + g, _ = errgroup.WithContext(ctx) + + createClusters() + + By("Setting up the Provider", func() { + _, sa1TokenMember := mustCreateAdminSAAndToken(ctx, cliMember, "sa1", "default") + execPluginOutput := fmt.Sprintf(`{ + "apiVersion": "client.authentication.k8s.io/v1beta1", + "kind": "ExecCredential", + "status": { + "token": "%s" + } + }`, sa1TokenMember) + + var err error + provider, err = New(Options{ + KubeconfigStrategyOption: kubeconfigstrategy.Option{ + CredentialsProvider: &kubeconfigstrategy.CredentialsProviderOption{ + Provider: credentials.New([]credentials.Provider{{ + Name: credentialProviderName, + ExecConfig: &clientcmdapi.ExecConfig{ + APIVersion: "client.authentication.k8s.io/v1beta1", + Command: "sh", + Args: []string{"-c", fmt.Sprintf("echo '%s'", execPluginOutput)}, + }, + }}), + }, + }, + }) + Expect(err).NotTo(HaveOccurred()) + Expect(provider).NotTo(BeNil()) + }) + + setupAndStartControllers() + + 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()) + + // Mock the control plane health condition and CredentialProviders + profileMember.Status.CredentialProviders = []clusterinventoryv1alpha1.CredentialProvider{{ + Name: credentialProviderName, + Cluster: clientcmdv1.Cluster{ + Server: cfgMember.Host, + CertificateAuthorityData: cfgMember.CAData, + }, + }} + 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()) + }) + + createObjects() }) - By("Waiting for the error group to finish", func() { - err := g.Wait() - Expect(err).NotTo(HaveOccurred()) + + assertBasicControllerBehavior() + assertClusterIndexBehavior() + + // No need to test for re-engaging the cluster since the kubeconfig is provided by the exec plugin + + 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()) + }) + shutDownClusters() }) }) }) @@ -326,7 +489,7 @@ func mustCreateAdminSAAndToken(ctx context.Context, cli client.Client, name, nam Namespace: namespace, }, } - Expect(cli.Create(ctx, &sa)).To(Succeed()) + Expect(client.IgnoreAlreadyExists(cli.Create(ctx, &sa))).To(Succeed()) tokenRequest := authenticationv1.TokenRequest{ Spec: authenticationv1.TokenRequestSpec{ @@ -353,7 +516,7 @@ func mustCreateAdminSAAndToken(ctx context.Context, cli client.Client, name, nam Name: "cluster-admin", }, } - Expect(cli.Create(ctx, &adminClusterRoleBinding)).To(Succeed()) + Expect(client.IgnoreAlreadyExists(cli.Create(ctx, &adminClusterRoleBinding))).To(Succeed()) return sa, tokenRequest.Status.Token } diff --git a/providers/cluster-inventory-api/suite_test.go b/providers/cluster-inventory-api/suite_test.go index 21b2743..0cbb107 100644 --- a/providers/cluster-inventory-api/suite_test.go +++ b/providers/cluster-inventory-api/suite_test.go @@ -25,9 +25,7 @@ import ( "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" @@ -43,11 +41,7 @@ func TestBuilder(t *testing.T) { RunSpecs(t, "Cluster Inventory API Provider Suite") } -var testenvHub *envtest.Environment -var cfgHub *rest.Config - -var testenvMember *envtest.Environment -var cfgMember *rest.Config +var clusterProfileCRDPath string var _ = BeforeSuite(func() { runtime.Must(clusterinventoryv1alpha1.AddToScheme(scheme.Scheme)) @@ -56,36 +50,17 @@ var _ = BeforeSuite(func() { assetDir := GinkgoT().TempDir() - clusterProfileCRDPath := filepath.Join(assetDir, "multicluster.x-k8s.io_clusterprofiles.yaml") + 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" }) diff --git a/providers/clusterprofile/clusterprofile_suite_test.go b/providers/clusterprofile/clusterprofile_suite_test.go deleted file mode 100644 index fc3f699..0000000 --- a/providers/clusterprofile/clusterprofile_suite_test.go +++ /dev/null @@ -1,112 +0,0 @@ -/* -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 clusterprofile - -import ( - "testing" - - "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" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -func TestBuilder(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "ClusterProfiles Provider Suite") -} - -var localEnv *envtest.Environment -var localCfg *rest.Config - -var zooEnv *envtest.Environment -var zooCfg *rest.Config - -var jungleEnv *envtest.Environment -var jungleCfg *rest.Config - -var islandEnv *envtest.Environment -var islandCfg *rest.Config - -var _ = BeforeSuite(func() { - logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) - - var err error - - // 'local' cluster runs the manager - localEnv = &envtest.Environment{ - UseExistingCluster: &[]bool{true}[0], - } - localCfg, err = localEnv.Start() - if err != nil { - Skip("Skipping tests due to missing kubebuilder/envtest setup: " + err.Error()) - } - - // startup 'remote' clusters - zooEnv = &envtest.Environment{ - UseExistingCluster: &[]bool{true}[0], - } - zooCfg, err = zooEnv.Start() - if err != nil { - Skip("Skipping tests due to missing kubebuilder/envtest setup: " + err.Error()) - } - - jungleEnv = &envtest.Environment{ - UseExistingCluster: &[]bool{true}[0], - } - jungleCfg, err = jungleEnv.Start() - if err != nil { - Skip("Skipping tests due to missing kubebuilder/envtest setup: " + err.Error()) - } - - islandEnv = &envtest.Environment{ - UseExistingCluster: &[]bool{true}[0], - } - islandCfg, err = islandEnv.Start() - if err != nil { - Skip("Skipping tests due to missing kubebuilder/envtest setup: " + err.Error()) - } - - // Prevent the metrics listener being created - metricsserver.DefaultBindAddress = "0" -}) - -var _ = AfterSuite(func() { - if localEnv != nil { - Expect(localEnv.Stop()).To(Succeed()) - } - - if zooEnv != nil { - Expect(zooEnv.Stop()).To(Succeed()) - } - - if jungleEnv != nil { - Expect(jungleEnv.Stop()).To(Succeed()) - } - - if islandEnv != nil { - Expect(islandEnv.Stop()).To(Succeed()) - } - - // Put the DefaultBindAddress back - metricsserver.DefaultBindAddress = ":8080" -}) diff --git a/providers/clusterprofile/provider.go b/providers/clusterprofile/provider.go deleted file mode 100644 index b5cb802..0000000 --- a/providers/clusterprofile/provider.go +++ /dev/null @@ -1,358 +0,0 @@ -/* -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 clusterprofile provides a Kubernetes cluster provider that watches ClusterProfile -// resources and creates controller-runtime clusters for each using credential providers. -package clusterprofile - -import ( - "context" - "encoding/json" - "fmt" - "hash/fnv" - "sync" - - "github.com/go-logr/logr" - clusterinventory "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1" - "sigs.k8s.io/cluster-inventory-api/pkg/credentials" - - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/kubernetes/scheme" - "k8s.io/client-go/rest" - - ctrl "sigs.k8s.io/controller-runtime" - "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/log" - "sigs.k8s.io/controller-runtime/pkg/predicate" - - mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" - "sigs.k8s.io/multicluster-runtime/pkg/multicluster" -) - -var _ multicluster.Provider = &Provider{} - -// New creates a new ClusterProfile provider. -func New(opts Options) *Provider { - if opts.Scheme == nil { - opts.Scheme = scheme.Scheme - } - - if opts.CredentialsProvider == nil { - opts.CredentialsProvider = credentials.New([]credentials.Provider{}) - } - - return &Provider{ - opts: opts, - log: log.Log.WithName("clusterprofile-provider"), - clusters: map[string]activeCluster{}, - } -} - -// Options contains the configuration for the clusterprofile provider. -type Options struct { - // Namespace is the cluster inventory namespace. - Namespace string - // Scheme is the scheme to use for the clusters. - Scheme *runtime.Scheme - // CredentialsProvider is the credentials provider for the cluster profiles. - CredentialsProvider *credentials.CredentialsProvider -} - -type index struct { - object client.Object - field string - extractValue client.IndexerFunc -} - -// Provider is a cluster provider that watches for ClusterProfile resources -// and engages clusters based on their credential providers. -type Provider struct { - opts Options - log logr.Logger - lock sync.RWMutex // protects clusters and indexers - clusters map[string]activeCluster - indexers []index - mgr mcmanager.Manager -} - -type activeCluster struct { - Cluster cluster.Cluster - Context context.Context - Cancel context.CancelFunc - Hash string -} - -func (p *Provider) getCluster(clusterName string) (activeCluster, bool) { - p.lock.RLock() - defer p.lock.RUnlock() - - ac, exists := p.clusters[clusterName] - return ac, exists -} - -func (p *Provider) setCluster(clusterName string, ac activeCluster) { - p.lock.Lock() - defer p.lock.Unlock() - - p.clusters[clusterName] = ac -} - -func (p *Provider) addIndexer(idx index) { - p.lock.Lock() - defer p.lock.Unlock() - - p.indexers = append(p.indexers, idx) -} - -// Get returns the cluster with the given name, if it is known. -func (p *Provider) Get(ctx context.Context, clusterName string) (cluster.Cluster, error) { - ac, exists := p.getCluster(clusterName) - if !exists { - return nil, multicluster.ErrClusterNotFound - } - return ac.Cluster, nil -} - -// SetupWithManager sets up the provider with the manager. -func (p *Provider) SetupWithManager(ctx context.Context, mgr mcmanager.Manager) error { - log := p.log - log.Info("Starting clusterprofile provider", "options", p.opts) - - if mgr == nil { - return fmt.Errorf("manager is nil") - } - p.mgr = mgr - - // Get the local manager from the multicluster manager - localMgr := mgr.GetLocalManager() - if localMgr == nil { - return fmt.Errorf("local manager is nil") - } - - // Setup the controller to watch for cluster profiles. - err := ctrl.NewControllerManagedBy(localMgr). - For(&clusterinventory.ClusterProfile{}, builder.WithPredicates(predicate.NewPredicateFuncs( - func(obj client.Object) bool { - if p.opts.Namespace == "" { - return true - } - return obj.GetNamespace() == p.opts.Namespace - }, - ))). - Complete(p) - if err != nil { - return fmt.Errorf("failed to create controller: %w", err) - } - - return nil -} - -// Reconcile is the main controller function that reconciles cluster profiles -func (p *Provider) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - cp, err := p.getClusterProfile(ctx, req.NamespacedName) - if err != nil { - return ctrl.Result{}, err - } - - if cp == nil || cp.DeletionTimestamp != nil { - p.removeCluster(req.Name) - return ctrl.Result{}, nil - } - - clusterName := req.NamespacedName.String() - log := p.log.WithValues("cluster", clusterName) - - hashStr, err := p.hashClusterProfile(cp) - if err != nil { - return ctrl.Result{}, err - } - - // Check if cluster exists and needs to be updated - existingCluster, clusterExists := p.getCluster(clusterName) - if clusterExists { - if existingCluster.Hash == hashStr { - log.Info("Cluster already exists and has the hash, skipping") - return ctrl.Result{}, nil - } - // If the cluster exists and the configuration has changed, - // remove it and continue to create a new cluster in its place. - // Creating a new cluster will ensure all new configuration is applied. - log.Info("Cluster already exists, updating it") - p.removeCluster(clusterName) - } - - restConfig, err := p.opts.CredentialsProvider.BuildConfigFromCP(cp) - if err != nil { - return ctrl.Result{}, fmt.Errorf("failed to get REST config for cluster %q: %w", clusterName, err) - } - - // Create and setup the new cluster - if err := p.createAndEngageCluster(ctx, clusterName, restConfig, hashStr, log); err != nil { - return ctrl.Result{}, err - } - - return ctrl.Result{}, nil -} - -// getClusterProfile retrieves a cluster profile and handles not found errors -func (p *Provider) getClusterProfile(ctx context.Context, namespacedName client.ObjectKey) (*clusterinventory.ClusterProfile, error) { - cp := &clusterinventory.ClusterProfile{} - if err := p.mgr.GetLocalManager().GetClient().Get(ctx, namespacedName, cp); err != nil { - if apierrors.IsNotFound(err) { - return nil, nil - } - return nil, fmt.Errorf("failed to get cluster profile: %w", err) - } - return cp, nil -} - -func (p *Provider) hashClusterProfile(cp *clusterinventory.ClusterProfile) (string, error) { - hash := fnv.New32a() - cpJSON, err := json.Marshal(cp) - if err != nil { - return "", fmt.Errorf("failed to marshal cluster profile: %w", err) - } - hash.Write(cpJSON) - return fmt.Sprintf("%x", hash.Sum32()), nil -} - -// createAndEngageCluster creates a new cluster, sets it up, stores it, and engages it with the manager -func (p *Provider) createAndEngageCluster(ctx context.Context, clusterName string, restConfig *rest.Config, hashStr string, log logr.Logger) error { - // Create a new cluster - log.Info("Creating new cluster from REST config") - cl, err := cluster.New(restConfig, func(o *cluster.Options) { - o.Scheme = p.opts.Scheme - }) - if err != nil { - return fmt.Errorf("failed to create cluster: %w", err) - } - - // Apply field indexers - if err := p.applyIndexers(ctx, cl); err != nil { - return err - } - - // Create a context that will be canceled when this cluster is removed - clusterCtx, cancel := context.WithCancel(ctx) - - // Start the cluster - go func() { - if err := cl.Start(clusterCtx); err != nil { - log.Error(err, "Failed to start cluster") - } - }() - - // Wait for cache to be ready - log.Info("Waiting for cluster cache to be ready") - if !cl.GetCache().WaitForCacheSync(clusterCtx) { - cancel() - return fmt.Errorf("failed to wait for cache sync") - } - log.Info("Cluster cache is ready") - - // Store the cluster - p.setCluster(clusterName, activeCluster{ - Cluster: cl, - Context: clusterCtx, - Cancel: cancel, - Hash: hashStr, - }) - - log.Info("Successfully added cluster") - - // Engage cluster so that the manager can start operating on the cluster - if err := p.mgr.Engage(clusterCtx, clusterName, cl); err != nil { - log.Error(err, "Failed to engage manager, removing cluster") - p.removeCluster(clusterName) - return fmt.Errorf("failed to engage manager: %w", err) - } - - log.Info("Successfully engaged manager") - return nil -} - -// applyIndexers applies field indexers to a cluster -func (p *Provider) applyIndexers(ctx context.Context, cl cluster.Cluster) error { - p.lock.RLock() - defer p.lock.RUnlock() - - for _, idx := range p.indexers { - if err := cl.GetFieldIndexer().IndexField(ctx, idx.object, idx.field, idx.extractValue); err != nil { - return fmt.Errorf("failed to index field %q: %w", idx.field, err) - } - } - - return 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 { - // Save for future clusters - p.addIndexer(index{ - object: obj, - field: field, - extractValue: extractValue, - }) - - // Apply to existing clusters - p.lock.RLock() - defer p.lock.RUnlock() - - for name, ac := range p.clusters { - if err := ac.Cluster.GetFieldIndexer().IndexField(ctx, obj, field, extractValue); err != nil { - return fmt.Errorf("failed to index field %q on cluster %q: %w", field, name, err) - } - } - - return nil -} - -// ListClusters returns a list of all discovered clusters. -func (p *Provider) ListClusters() []string { - p.lock.RLock() - defer p.lock.RUnlock() - - result := make([]string, 0, len(p.clusters)) - for name := range p.clusters { - result = append(result, name) - } - return result -} - -// removeCluster removes a cluster by name with write lock and cleanup -func (p *Provider) removeCluster(clusterName string) { - log := p.log.WithValues("cluster", clusterName) - - p.lock.Lock() - ac, exists := p.clusters[clusterName] - if !exists { - p.lock.Unlock() - log.Info("Cluster not found, nothing to remove") - return - } - - log.Info("Removing cluster") - delete(p.clusters, clusterName) - p.lock.Unlock() - - // Cancel the context to trigger cleanup for this cluster. - // This is done outside the lock to avoid holding the lock for a long time. - ac.Cancel() - log.Info("Successfully removed cluster and cancelled cluster context") -} diff --git a/providers/clusterprofile/provider_test.go b/providers/clusterprofile/provider_test.go deleted file mode 100644 index 0c0de48..0000000 --- a/providers/clusterprofile/provider_test.go +++ /dev/null @@ -1,431 +0,0 @@ -/* -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 clusterprofile - -import ( - "context" - "errors" - "fmt" - "strconv" - "sync" - "time" - - clusterinventory "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1" - "sigs.k8s.io/cluster-inventory-api/pkg/credentials" - - corev1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - "k8s.io/client-go/rest" - clientcmdapi "k8s.io/client-go/tools/clientcmd/api" - clientcmdv1 "k8s.io/client-go/tools/clientcmd/api/v1" - "k8s.io/client-go/util/retry" - - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/cluster" - "sigs.k8s.io/controller-runtime/pkg/log" - 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" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -const clusterInventoryNamespace = "testing" - -var _ = Describe("Provider Namespace", Ordered, func() { - ctx, cancel := context.WithCancel(context.Background()) - wg := sync.WaitGroup{} - - var provider *Provider - var mgr mcmanager.Manager - var localCli client.Client - var zooCli client.Client - var jungleCli client.Client - var islandCli client.Client - - BeforeAll(func() { - var err error - localCli, err = client.New(localCfg, client.Options{}) - Expect(err).NotTo(HaveOccurred()) - zooCli, err = client.New(zooCfg, client.Options{}) - Expect(err).NotTo(HaveOccurred()) - jungleCli, err = client.New(jungleCfg, client.Options{}) - Expect(err).NotTo(HaveOccurred()) - islandCli, err = client.New(islandCfg, client.Options{}) - Expect(err).NotTo(HaveOccurred()) - - provider = New(Options{ - Namespace: clusterInventoryNamespace, - CredentialsProvider: credentials.New([]credentials.Provider{ - { - Name: "foobar", - ExecConfig: &clientcmdapi.ExecConfig{ - Command: "test-command-1", - Args: []string{"arg1", "arg2"}, - APIVersion: "client.authentication.k8s.io/v1beta1", - }, - }, - }), - }) - - By("Creating a namespace in the local cluster", func() { - namespace := &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: clusterInventoryNamespace, - }, - } - - err = localCli.Create(ctx, namespace) - Expect(err).NotTo(HaveOccurred()) - }) - - By("Creating cluster profiles in the local cluster", func() { - err = createClusterProfile(ctx, "zoo", zooCfg, localCli) - Expect(err).NotTo(HaveOccurred()) - - err = createClusterProfile(ctx, "jungle", jungleCfg, localCli) - Expect(err).NotTo(HaveOccurred()) - - err = createClusterProfile(ctx, "island", islandCfg, localCli) - Expect(err).NotTo(HaveOccurred()) - }) - - By("Setting up the cluster-aware manager, with the provider to lookup clusters", func() { - var err error - mgr, err = mcmanager.New(localCfg, provider, mcmanager.Options{ - Metrics: metricsserver.Options{ - BindAddress: "0", - }, - }) - Expect(err).NotTo(HaveOccurred()) - }) - - By("Setting up the provider with the manager", func() { - err := provider.SetupWithManager(ctx, mgr) - Expect(err).NotTo(HaveOccurred()) - }) - - By("Setting up the controller feeding the animals", func() { - err := mcbuilder.ControllerManagedBy(mgr). - Named("fleet-ns-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) - } - - 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() { - wg.Add(1) - go func() { - err := ignoreCanceled(mgr.Start(ctx)) - Expect(err).NotTo(HaveOccurred()) - wg.Done() - }() - }) - - }) - - BeforeAll(func() { - utilruntime.Must(client.IgnoreAlreadyExists(zooCli.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "zoo"}}))) - utilruntime.Must(client.IgnoreAlreadyExists(zooCli.Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: "zoo", Name: "elephant", Labels: map[string]string{"type": "animal"}}}))) - utilruntime.Must(client.IgnoreAlreadyExists(zooCli.Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: "zoo", Name: "lion", Labels: map[string]string{"type": "animal"}}}))) - utilruntime.Must(client.IgnoreAlreadyExists(zooCli.Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: "zoo", Name: "keeper", Labels: map[string]string{"type": "human"}}}))) - - utilruntime.Must(client.IgnoreAlreadyExists(jungleCli.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "jungle"}}))) - utilruntime.Must(client.IgnoreAlreadyExists(jungleCli.Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: "jungle", Name: "monkey", Labels: map[string]string{"type": "animal"}}}))) - utilruntime.Must(client.IgnoreAlreadyExists(jungleCli.Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: "jungle", Name: "tree", Labels: map[string]string{"type": "thing"}}}))) - utilruntime.Must(client.IgnoreAlreadyExists(jungleCli.Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: "jungle", Name: "tarzan", Labels: map[string]string{"type": "human"}}}))) - - utilruntime.Must(client.IgnoreAlreadyExists(islandCli.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "island"}}))) - utilruntime.Must(client.IgnoreAlreadyExists(islandCli.Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: "island", Name: "bird", Labels: map[string]string{"type": "animal"}}}))) - utilruntime.Must(client.IgnoreAlreadyExists(islandCli.Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: "island", Name: "stone", Labels: map[string]string{"type": "thing"}}}))) - utilruntime.Must(client.IgnoreAlreadyExists(islandCli.Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: "island", Name: "crusoe", Labels: map[string]string{"type": "human"}}}))) - }) - - It("lists the clusters loaded from cluster profiles", func() { - Eventually(provider.ListClusters, "10s").Should(HaveLen(3)) - }) - - It("runs the reconciler for existing objects", func(ctx context.Context) { - Eventually(func() string { - lion := &corev1.ConfigMap{} - err := zooCli.Get(ctx, client.ObjectKey{Namespace: "zoo", Name: "lion"}, 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 := zooCli.Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: "zoo", Name: "tiger", Labels: map[string]string{"type": "animal"}}}) - Expect(err).NotTo(HaveOccurred()) - }) - - Eventually(func() string { - tiger := &corev1.ConfigMap{} - err := zooCli.Get(ctx, client.ObjectKey{Namespace: "zoo", Name: "tiger"}, 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 elephant's stomach", func() { - err := retry.RetryOnConflict(retry.DefaultRetry, func() error { - if err := zooCli.Get(ctx, client.ObjectKey{Namespace: "zoo", Name: "elephant"}, updated); err != nil { - return err - } - updated.Data = map[string]string{} - return zooCli.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 := zooCli.Get(ctx, client.ObjectKey{Namespace: "zoo", Name: "elephant"}, 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 := zooCli.Get(ctx, client.ObjectKey{Namespace: "zoo", Name: "elephant"}, elephant) - Expect(err).NotTo(HaveOccurred()) - return elephant.Data["stomach"] - }, "10s").Should(Equal("food")) - }) - - It("queries one cluster via a multi-cluster index", func() { - island, err := mgr.GetCluster(ctx, "island") - Expect(err).NotTo(HaveOccurred()) - - cms := &corev1.ConfigMapList{} - err = island.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("crusoe")) - Expect(cms.Items[0].Namespace).To(Equal("island")) - }) - - It("reconciles objects when the cluster is updated in cluster profile", func() { - islandClusterProfile := &clusterinventory.ClusterProfile{} - err := localCli.Get(ctx, client.ObjectKey{Name: "island", Namespace: clusterInventoryNamespace}, islandClusterProfile) - Expect(err).NotTo(HaveOccurred()) - - // Update the cluster profile to point to jungle configuration - islandClusterProfile.Status.CredentialProviders = []clusterinventory.CredentialProvider{ - { - Name: "foobar", - Cluster: clientcmdv1.Cluster{ - Server: jungleCfg.Host, - CertificateAuthorityData: jungleCfg.CAData, - }, - }, - } - err = localCli.Status().Update(ctx, islandClusterProfile) - Expect(err).NotTo(HaveOccurred()) - - err = jungleCli.Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: "jungle", Name: "dog", Labels: map[string]string{"type": "animal"}}}) - Expect(err).NotTo(HaveOccurred()) - - Eventually(func() string { - dog := &corev1.ConfigMap{} - err := jungleCli.Get(ctx, client.ObjectKey{Namespace: "jungle", Name: "dog"}, dog) - Expect(err).NotTo(HaveOccurred()) - return dog.Data["stomach"] - }, "10s").Should(Equal("food")) - }) - - It("reconciles objects when cluster profile is updated without changing the cluster", func() { - jungleClusterProfile := &clusterinventory.ClusterProfile{} - err := localCli.Get(ctx, client.ObjectKey{Name: "jungle", Namespace: clusterInventoryNamespace}, jungleClusterProfile) - Expect(err).NotTo(HaveOccurred()) - - jungleClusterProfile.ObjectMeta.Annotations = map[string]string{ - "location": "amazon", - } - err = localCli.Update(ctx, jungleClusterProfile) - Expect(err).NotTo(HaveOccurred()) - - err = jungleCli.Create(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: "jungle", Name: "leopard", Labels: map[string]string{"type": "animal"}}}) - Expect(err).NotTo(HaveOccurred()) - - Eventually(func() string { - leopard := &corev1.ConfigMap{} - err := jungleCli.Get(ctx, client.ObjectKey{Namespace: "jungle", Name: "leopard"}, leopard) - Expect(err).NotTo(HaveOccurred()) - return leopard.Data["stomach"] - }, "10s").Should(Equal("food")) - }) - - It("removes a cluster from the provider when the cluster profile is deleted", func() { - err := localCli.Delete(ctx, &clusterinventory.ClusterProfile{ObjectMeta: metav1.ObjectMeta{Name: "island", Namespace: clusterInventoryNamespace}}) - Expect(err).NotTo(HaveOccurred()) - Eventually(provider.ListClusters, "10s").Should(HaveLen(2)) - }) - - AfterAll(func() { - By("Stopping the provider, cluster, manager, and controller", func() { - cancel() - wg.Wait() - }) - }) -}) - -var _ = Describe("Provider race condition", func() { - It("should handle concurrent operations without issues", func() { - p := New(Options{}) - - // Pre-populate with some clusters to make the test meaningful - numClusters := 20 - for i := 0; i < numClusters; i++ { - clusterName := fmt.Sprintf("cluster-%d", i) - p.clusters[clusterName] = activeCluster{ - Cluster: &mockCluster{}, - Cancel: func() {}, - } - } - - var wg sync.WaitGroup - numGoroutines := 40 - wg.Add(numGoroutines) - - for i := 0; i < numGoroutines; i++ { - go func(i int) { - defer GinkgoRecover() - defer wg.Done() - - // Mix of operations to stress the provider - switch i % 4 { - case 0: - // Concurrently index a field. This will read the cluster list. - err := p.IndexField(context.Background(), &corev1.Pod{}, "spec.nodeName", func(rawObj client.Object) []string { - return nil - }) - Expect(err).NotTo(HaveOccurred()) - case 1: - // Concurrently get a cluster. - _, err := p.Get(context.Background(), "cluster-1") - Expect(err).To(Or(BeNil(), MatchError("cluster cluster-1 not found"))) - case 2: - // Concurrently list clusters. - p.ListClusters() - case 3: - // Concurrently delete a cluster. This will modify the cluster map. - clusterToRemove := fmt.Sprintf("cluster-%d", i/4) - p.removeCluster(clusterToRemove) - } - }(i) - } - - wg.Wait() - }) -}) - -func ignoreCanceled(err error) error { - if errors.Is(err, context.Canceled) { - return nil - } - return err -} - -func createClusterProfile(ctx context.Context, name string, cfg *rest.Config, cl client.Client) error { - cp := &clusterinventory.ClusterProfile{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: clusterInventoryNamespace, - }, - Status: clusterinventory.ClusterProfileStatus{ - CredentialProviders: []clusterinventory.CredentialProvider{ - { - Name: "foobar", - Cluster: clientcmdv1.Cluster{ - Server: cfg.Host, - CertificateAuthorityData: cfg.CAData, - }, - }, - }, - }, - } - - return cl.Create(ctx, cp) -} - -// mockCluster is a mock implementation of cluster.Cluster for testing. -type mockCluster struct { - cluster.Cluster -} - -func (c *mockCluster) GetFieldIndexer() client.FieldIndexer { - return &mockFieldIndexer{} -} - -type mockFieldIndexer struct{} - -func (f *mockFieldIndexer) IndexField(ctx context.Context, obj client.Object, field string, extractValue client.IndexerFunc) error { - // Simulate work to increase chance of race - time.Sleep(time.Millisecond) - return nil -} From 8cd2845122a07378d0d010e2cf697ed28e4d5865 Mon Sep 17 00:00:00 2001 From: Shingo Omura Date: Sat, 26 Jul 2025 03:28:23 +0900 Subject: [PATCH 15/18] add {providers, examples}/cluster-inventory-api in ci.yaml Signed-off-by: Shingo Omura --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a4cbc31..8186292 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,8 @@ jobs: - providers/kind - examples/cluster-api - providers/cluster-api + - examples/cluster-inventory-api + - providers/cluster-inventory-api name: golangci-lint [${{ matrix.working-directory }}] steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # tag=v4.2.2 From 8280ec35538e3d213e3b433920e97ad53c4add14 Mon Sep 17 00:00:00 2001 From: Shingo Omura Date: Sat, 26 Jul 2025 03:47:28 +0900 Subject: [PATCH 16/18] Fix lint errors --- examples/cluster-inventory-api/main.go | 1 - .../kubeconfigstrategy/credentialsprovider.go | 6 ++--- .../kubeconfigstrategy/interface.go | 4 ++-- .../kubeconfigstrategy/secret.go | 12 ++++++---- providers/cluster-inventory-api/provider.go | 8 +++---- .../cluster-inventory-api/provider_test.go | 23 ++++++++----------- providers/cluster-inventory-api/suite_test.go | 4 ++-- 7 files changed, 28 insertions(+), 30 deletions(-) diff --git a/examples/cluster-inventory-api/main.go b/examples/cluster-inventory-api/main.go index af357af..abe06f5 100644 --- a/examples/cluster-inventory-api/main.go +++ b/examples/cluster-inventory-api/main.go @@ -42,7 +42,6 @@ import ( 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" "sigs.k8s.io/multicluster-runtime/providers/cluster-inventory-api/kubeconfigstrategy" ) diff --git a/providers/cluster-inventory-api/kubeconfigstrategy/credentialsprovider.go b/providers/cluster-inventory-api/kubeconfigstrategy/credentialsprovider.go index 0ff3762..5b5c7d9 100644 --- a/providers/cluster-inventory-api/kubeconfigstrategy/credentialsprovider.go +++ b/providers/cluster-inventory-api/kubeconfigstrategy/credentialsprovider.go @@ -3,13 +3,13 @@ package kubeconfigstrategy import ( "context" + "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1" + "sigs.k8s.io/cluster-inventory-api/pkg/credentials" + "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" - - "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1" - "sigs.k8s.io/cluster-inventory-api/pkg/credentials" ) var _ Interface = &credentialsProviderStrategy{} diff --git a/providers/cluster-inventory-api/kubeconfigstrategy/interface.go b/providers/cluster-inventory-api/kubeconfigstrategy/interface.go index 1a443c0..c529822 100644 --- a/providers/cluster-inventory-api/kubeconfigstrategy/interface.go +++ b/providers/cluster-inventory-api/kubeconfigstrategy/interface.go @@ -3,10 +3,10 @@ package kubeconfigstrategy import ( "context" - "k8s.io/client-go/rest" - clusterinventoryv1alpha1 "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/handler" diff --git a/providers/cluster-inventory-api/kubeconfigstrategy/secret.go b/providers/cluster-inventory-api/kubeconfigstrategy/secret.go index cd68346..d15cad8 100644 --- a/providers/cluster-inventory-api/kubeconfigstrategy/secret.go +++ b/providers/cluster-inventory-api/kubeconfigstrategy/secret.go @@ -4,6 +4,8 @@ import ( "context" "fmt" + "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/rest" @@ -15,14 +17,15 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" - - "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1" ) const ( + // SecretLabelKeyClusterInventoryConsumer is the label key used to identify the consumer of the kubeconfig Secret. SecretLabelKeyClusterInventoryConsumer = "x-k8s.io/cluster-inventory-consumer" - SecretLabelKeyClusterProfile = "x-k8s.io/cluster-profile" - SecretDataKeyKubeConfig = "Config" // data key in the Secret that contains the kubeconfig. + // SecretLabelKeyClusterProfile is the label key used to identify the ClusterProfile associated with the kubeconfig Secret. + SecretLabelKeyClusterProfile = "x-k8s.io/cluster-profile" + // SecretDataKeyKubeConfig is the key in the Secret data that contains the kubeconfig. + SecretDataKeyKubeConfig = "Config" ) var _ Interface = &secretStrategy{} @@ -31,6 +34,7 @@ type secretStrategy struct { consumerName string } +// SecretStrategyOption holds options for the Secret strategy. type SecretStrategyOption struct { ConsumerName string } diff --git a/providers/cluster-inventory-api/provider.go b/providers/cluster-inventory-api/provider.go index f8924a6..72d33d4 100644 --- a/providers/cluster-inventory-api/provider.go +++ b/providers/cluster-inventory-api/provider.go @@ -24,6 +24,7 @@ import ( "time" "github.com/go-logr/logr" + clusterinventoryv1alpha1 "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" @@ -39,9 +40,6 @@ import ( mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" "sigs.k8s.io/multicluster-runtime/pkg/multicluster" - - clusterinventoryv1alpha1 "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1" - "sigs.k8s.io/multicluster-runtime/providers/cluster-inventory-api/kubeconfigstrategy" ) @@ -82,7 +80,7 @@ type Provider struct { indexers []index } -func setDefaults(opts *Options, cli client.Client) { +func setDefaults(opts *Options) { 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...) @@ -107,7 +105,7 @@ func New(opts Options) (*Provider, error) { kubeconfig: map[string]*rest.Config{}, strategy: strategy, } - setDefaults(&p.opts, p.client) + setDefaults(&p.opts) return p, nil } diff --git a/providers/cluster-inventory-api/provider_test.go b/providers/cluster-inventory-api/provider_test.go index fb32356..f2b4fc6 100644 --- a/providers/cluster-inventory-api/provider_test.go +++ b/providers/cluster-inventory-api/provider_test.go @@ -25,6 +25,8 @@ import ( "time" "golang.org/x/sync/errgroup" + clusterinventoryv1alpha1 "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1" + "sigs.k8s.io/cluster-inventory-api/pkg/credentials" authenticationv1 "k8s.io/api/authentication/v1" corev1 "k8s.io/api/core/v1" @@ -50,10 +52,6 @@ import ( mccontroller "sigs.k8s.io/multicluster-runtime/pkg/controller" mcmanager "sigs.k8s.io/multicluster-runtime/pkg/manager" mcreconcile "sigs.k8s.io/multicluster-runtime/pkg/reconcile" - - clusterinventoryv1alpha1 "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1" - "sigs.k8s.io/cluster-inventory-api/pkg/credentials" - "sigs.k8s.io/multicluster-runtime/providers/cluster-inventory-api/kubeconfigstrategy" . "github.com/onsi/ginkgo/v2" @@ -323,8 +321,8 @@ var _ = Describe("Provider Cluster Inventory API", Ordered, func() { }) Expect(cliHub.Status().Update(ctx, profileMember)).To(Succeed()) - _, sa1TokenMember = mustCreateAdminSAAndToken(ctx, cliMember, "sa1", "default") - _ = mustCreateOrUpdateKubeConfigSecretFromTokenSecret( + sa1TokenMember = mustCreateAdminSAAndToken(ctx, cliMember, "sa1", "default") + mustCreateOrUpdateKubeConfigSecretFromTokenSecret( ctx, cliHub, cfgMember, consumerName, *profileMember, @@ -340,8 +338,8 @@ var _ = Describe("Provider Cluster Inventory API", Ordered, func() { 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( + sa2TokenMember = mustCreateAdminSAAndToken(ctx, cliMember, "sa2", "default") + mustCreateOrUpdateKubeConfigSecretFromTokenSecret( ctx, cliHub, cfgMember, consumerName, *profileMember, @@ -391,7 +389,7 @@ var _ = Describe("Provider Cluster Inventory API", Ordered, func() { createClusters() By("Setting up the Provider", func() { - _, sa1TokenMember := mustCreateAdminSAAndToken(ctx, cliMember, "sa1", "default") + sa1TokenMember := mustCreateAdminSAAndToken(ctx, cliMember, "sa1", "default") execPluginOutput := fmt.Sprintf(`{ "apiVersion": "client.authentication.k8s.io/v1beta1", "kind": "ExecCredential", @@ -482,7 +480,7 @@ func ignoreCanceled(err error) error { return err } -func mustCreateAdminSAAndToken(ctx context.Context, cli client.Client, name, namespace string) (corev1.ServiceAccount, string) { +func mustCreateAdminSAAndToken(ctx context.Context, cli client.Client, name, namespace string) string { sa := corev1.ServiceAccount{ ObjectMeta: metav1.ObjectMeta{ Name: name, @@ -518,7 +516,7 @@ func mustCreateAdminSAAndToken(ctx context.Context, cli client.Client, name, nam } Expect(client.IgnoreAlreadyExists(cli.Create(ctx, &adminClusterRoleBinding))).To(Succeed()) - return sa, tokenRequest.Status.Token + return tokenRequest.Status.Token } func mustCreateOrUpdateKubeConfigSecretFromTokenSecret( @@ -528,7 +526,7 @@ func mustCreateOrUpdateKubeConfigSecretFromTokenSecret( consumerName string, clusterProfile clusterinventoryv1alpha1.ClusterProfile, token string, -) corev1.Secret { +) { kubeconfigStr := fmt.Sprintf(`apiVersion: v1 clusters: - cluster: @@ -568,5 +566,4 @@ users: 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 index 0cbb107..f12ac65 100644 --- a/providers/cluster-inventory-api/suite_test.go +++ b/providers/cluster-inventory-api/suite_test.go @@ -23,6 +23,8 @@ import ( "path/filepath" "testing" + clusterinventoryv1alpha1 "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1" + "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/client-go/kubernetes/scheme" @@ -30,8 +32,6 @@ import ( "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" ) From 23e0799a4e30f1be318bc390f9c82e0df40232eb Mon Sep 17 00:00:00 2001 From: Shingo Omura Date: Mon, 28 Jul 2025 22:30:35 +0900 Subject: [PATCH 17/18] Update examples/cluster-inventory-api/main.go Co-authored-by: Marvin Beckers --- examples/cluster-inventory-api/main.go | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/examples/cluster-inventory-api/main.go b/examples/cluster-inventory-api/main.go index abe06f5..5755f4c 100644 --- a/examples/cluster-inventory-api/main.go +++ b/examples/cluster-inventory-api/main.go @@ -22,7 +22,6 @@ import ( "flag" "os" - "golang.org/x/sync/errgroup" clusterinventoryv1alpha1 "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1" "sigs.k8s.io/cluster-inventory-api/pkg/credentials" @@ -141,12 +140,7 @@ func main() { os.Exit(1) } - // Starting everything. - g, ctx := errgroup.WithContext(ctx) - g.Go(func() error { - return ignoreCanceled(mcMgr.Start(ctx)) - }) - if err := g.Wait(); err != nil { + if err := mcMgr.Start(ctx); ignoreCanceled(err) != nil { entryLog.Error(err, "unable to start") os.Exit(1) } From c35aa8e917a1e33a1635b76ddf4b069075b4c327 Mon Sep 17 00:00:00 2001 From: Shingo Omura Date: Mon, 28 Jul 2025 22:56:26 +0900 Subject: [PATCH 18/18] re-create go.mod for /{providers,examples}/cluster-inventory-api and "make moduels" Signed-off-by: Shingo Omura --- examples/cluster-inventory-api/go.mod | 12 ++++++------ examples/cluster-inventory-api/go.sum | 16 ++++++++-------- providers/cluster-inventory-api/go.mod | 4 ++-- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/examples/cluster-inventory-api/go.mod b/examples/cluster-inventory-api/go.mod index 561c717..0e7f209 100644 --- a/examples/cluster-inventory-api/go.mod +++ b/examples/cluster-inventory-api/go.mod @@ -1,6 +1,6 @@ module sigs.k8s.io/multicluster-runtime/examples/cluster-inventory-api -go 1.24.2 +go 1.24.0 replace ( sigs.k8s.io/multicluster-runtime => ../.. @@ -8,13 +8,12 @@ replace ( ) 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 + 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-20250702132726-0f613c6275a5 sigs.k8s.io/controller-runtime v0.21.0 - sigs.k8s.io/multicluster-runtime v0.21.0-alpha.8 + sigs.k8s.io/multicluster-runtime v0.0.0-00010101000000-000000000000 sigs.k8s.io/multicluster-runtime/providers/cluster-inventory-api v0.0.0-00010101000000-000000000000 ) @@ -53,6 +52,7 @@ require ( go.uber.org/zap v1.27.0 // indirect golang.org/x/net v0.39.0 // indirect golang.org/x/oauth2 v0.29.0 // indirect + golang.org/x/sync v0.13.0 // indirect golang.org/x/sys v0.32.0 // indirect golang.org/x/term v0.31.0 // indirect golang.org/x/text v0.24.0 // indirect diff --git a/examples/cluster-inventory-api/go.sum b/examples/cluster-inventory-api/go.sum index 32d2ae6..f2dcdcc 100644 --- a/examples/cluster-inventory-api/go.sum +++ b/examples/cluster-inventory-api/go.sum @@ -125,8 +125,8 @@ golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT 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/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.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= @@ -165,14 +165,14 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 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/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.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/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= diff --git a/providers/cluster-inventory-api/go.mod b/providers/cluster-inventory-api/go.mod index 313a405..d60f161 100644 --- a/providers/cluster-inventory-api/go.mod +++ b/providers/cluster-inventory-api/go.mod @@ -1,6 +1,6 @@ module sigs.k8s.io/multicluster-runtime/providers/cluster-inventory-api -go 1.24.2 +go 1.24.0 replace sigs.k8s.io/multicluster-runtime => ../.. @@ -15,7 +15,7 @@ require ( k8s.io/utils v0.0.0-20241210054802-24370beab758 sigs.k8s.io/cluster-inventory-api v0.0.0-20250702132726-0f613c6275a5 sigs.k8s.io/controller-runtime v0.21.0 - sigs.k8s.io/multicluster-runtime v0.21.0-alpha.8 + sigs.k8s.io/multicluster-runtime v0.0.0-00010101000000-000000000000 ) require (