From 47d261a5c0e3f86fd81ddf0f34bf13eec343bc46 Mon Sep 17 00:00:00 2001 From: Hasan Turken Date: Wed, 22 May 2024 13:44:46 +0300 Subject: [PATCH 1/7] Add support for auth with Upbound identity Signed-off-by: Hasan Turken --- apis/v1alpha1/types.go | 4 +- ...r-config-with-secret-upbound-identity.yaml | 18 ++++ go.mod | 38 ++++---- go.sum | 74 ++++++++-------- internal/clients/kube/kube.go | 32 +++++-- internal/clients/upbound/upbound.go | 86 +++++++++++++++++++ ...ernetes.crossplane.io_providerconfigs.yaml | 1 + 7 files changed, 189 insertions(+), 64 deletions(-) create mode 100644 examples/provider/provider-config-with-secret-upbound-identity.yaml create mode 100644 internal/clients/upbound/upbound.go diff --git a/apis/v1alpha1/types.go b/apis/v1alpha1/types.go index fdde5fcc..9d83e5cb 100644 --- a/apis/v1alpha1/types.go +++ b/apis/v1alpha1/types.go @@ -53,12 +53,14 @@ const ( IdentityTypeAzureServicePrincipalCredentials = "AzureServicePrincipalCredentials" IdentityTypeAzureWorkloadIdentityCredentials = "AzureWorkloadIdentityCredentials" + + IdentityTypeUpboundToken = "UpboundToken" ) // Identity used to authenticate. type Identity struct { // Type of identity. - // +kubebuilder:validation:Enum=GoogleApplicationCredentials;AzureServicePrincipalCredentials;AzureWorkloadIdentityCredentials + // +kubebuilder:validation:Enum=GoogleApplicationCredentials;AzureServicePrincipalCredentials;AzureWorkloadIdentityCredentials;UpboundToken Type IdentityType `json:"type"` ProviderCredentials `json:",inline"` diff --git a/examples/provider/provider-config-with-secret-upbound-identity.yaml b/examples/provider/provider-config-with-secret-upbound-identity.yaml new file mode 100644 index 00000000..0b8d406c --- /dev/null +++ b/examples/provider/provider-config-with-secret-upbound-identity.yaml @@ -0,0 +1,18 @@ +apiVersion: kubernetes.crossplane.io/v1alpha1 +kind: ProviderConfig +metadata: + name: kubernetes-provider +spec: + credentials: + source: Secret + secretRef: + namespace: crossplane-system + name: cluster-config + key: kubeconfig + identity: + type: UpboundToken + source: Secret + secretRef: + name: upbound-credentials + namespace: crossplane-system + key: token diff --git a/go.mod b/go.mod index 2bbc5b46..0afccf94 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/crossplane-contrib/provider-kubernetes -go 1.21 +go 1.22.1 + +toolchain go1.22.3 require ( github.com/Azure/kubelogin v0.1.1 @@ -8,16 +10,17 @@ require ( github.com/crossplane/crossplane-runtime v1.17.0-rc.0.0.20240509182037-b31be7747c60 github.com/crossplane/crossplane-tools v0.0.0-20240522174801-1ad3d4c87f21 github.com/google/go-cmp v0.6.0 - github.com/google/uuid v1.5.0 + github.com/google/uuid v1.6.0 github.com/pkg/errors v0.9.1 github.com/spf13/pflag v1.0.5 + github.com/upbound/up-sdk-go v0.3.1-0.20240517133145-e5da98257888 go.uber.org/zap v1.26.0 golang.org/x/oauth2 v0.20.0 k8s.io/api v0.29.3 k8s.io/apimachinery v0.29.3 k8s.io/client-go v0.29.3 - k8s.io/utils v0.0.0-20230726121419-3b25d923346b - sigs.k8s.io/controller-runtime v0.17.0 + k8s.io/utils v0.0.0-20240102154912-e7106e64919e + sigs.k8s.io/controller-runtime v0.17.1 sigs.k8s.io/controller-tools v0.14.0 ) @@ -39,24 +42,24 @@ require ( github.com/blang/semver/v4 v4.0.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/dave/jennifer v1.7.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.11.2 // indirect github.com/evanphx/json-patch v5.6.0+incompatible // indirect - github.com/evanphx/json-patch/v5 v5.8.0 // indirect + github.com/evanphx/json-patch/v5 v5.9.0 // indirect github.com/fatih/color v1.16.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/zapr v1.3.0 // indirect - github.com/go-openapi/jsonpointer v0.19.6 // indirect - github.com/go-openapi/jsonreference v0.20.2 // indirect - github.com/go-openapi/swag v0.22.3 // indirect + github.com/go-openapi/jsonpointer v0.20.2 // indirect + github.com/go-openapi/jsonreference v0.20.4 // indirect + github.com/go-openapi/swag v0.22.9 // indirect github.com/gobuffalo/flect v1.0.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang-jwt/jwt/v5 v5.2.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/imdario/mergo v0.3.16 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -66,28 +69,27 @@ require ( github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/matttproud/golang_protobuf_extensions/v2 v2.0.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/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/prometheus/client_golang v1.18.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect - github.com/prometheus/common v0.45.0 // indirect + github.com/prometheus/common v0.46.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cobra v1.8.0 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.21.0 // indirect - golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect - golang.org/x/mod v0.14.0 // indirect + golang.org/x/exp v0.0.0-20240213143201-ec583247a57a // indirect + golang.org/x/mod v0.15.0 // indirect golang.org/x/net v0.23.0 // indirect golang.org/x/sys v0.18.0 // indirect golang.org/x/term v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.17.0 // indirect + golang.org/x/tools v0.18.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect @@ -96,8 +98,8 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.29.1 // indirect k8s.io/component-base v0.29.3 // indirect - k8s.io/klog/v2 v2.110.1 // indirect - k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect + k8s.io/klog/v2 v2.120.1 // indirect + k8s.io/kube-openapi v0.0.0-20240209001042-7a0d5b415232 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect sigs.k8s.io/yaml v1.4.0 // indirect diff --git a/go.sum b/go.sum index 0417988c..0b627bd3 100644 --- a/go.sum +++ b/go.sum @@ -39,7 +39,6 @@ github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2y github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/crossplane/crossplane-runtime v1.17.0-rc.0.0.20240509182037-b31be7747c60 h1:subdCU8vHUDkaQAYBKKddCT2HpEK4A9fFEJ3nhGoTBc= github.com/crossplane/crossplane-runtime v1.17.0-rc.0.0.20240509182037-b31be7747c60/go.mod h1:Pz2tdGVMF6KDGzHZOkvKro0nKc8EzK0sb/nSA7pH4Dc= github.com/crossplane/crossplane-tools v0.0.0-20240522174801-1ad3d4c87f21 h1:8wb7/zCbVPkeX68WbVESWJmSWQE5SZKzz0g9X4FlXRw= @@ -47,33 +46,33 @@ github.com/crossplane/crossplane-tools v0.0.0-20240522174801-1ad3d4c87f21/go.mod github.com/dave/jennifer v1.7.0 h1:uRbSBH9UTS64yXbh4FrMHfgfY762RD+C7bUPKODpSJE= github.com/dave/jennifer v1.7.0/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 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/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= -github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= -github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.11.2 h1:1onLa9DcsMYO9P+CXaL0dStDqQ2EHHXLiz+BtnqkLAU= +github.com/emicklei/go-restful/v3 v3.11.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/evanphx/json-patch/v5 v5.8.0 h1:lRj6N9Nci7MvzrXuX6HFzU8XjmhPiXPlsKEy1u0KQro= -github.com/evanphx/json-patch/v5 v5.8.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= +github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/frankban/quicktest v1.2.2 h1:xfmOhhoH5fGPgbEAlhLpJH9p0z/0Qizio9osmvn9IUY= github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20= 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/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/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.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= -github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= -github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= -github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= -github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= +github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= +github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdXSSgNeAhojU= +github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4= +github.com/go-openapi/swag v0.22.9 h1:XX2DssF+mQKM2DHsbgZK74y/zj4mo9I99+89xUmuZCE= +github.com/go-openapi/swag v0.22.9/go.mod h1:3/OXnFfnMAwBD099SwYRk7GD3xOrr1iL7d/XNLXVVwE= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/gobuffalo/flect v1.0.2 h1:eqjPGSo2WmjgY2XlpGwo2NXgL3RucAKo4k4qQMNA5sA= @@ -89,8 +88,8 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= -github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 h1:0VpGH+cDhbDtdcweoyCVsF3fhN8kejK6rFe/2FFX2nU= +github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49/go.mod h1:BkkQ4L1KS1xMt2aWSPStnn55ChGC0DPOn2FQYj+f25M= github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= @@ -100,8 +99,8 @@ 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-20240117000934-35fc243c5815 h1:WzfWbQz/Ze8v6l++GGbGNFZnUShVpP/0xffCPLL+ax8= github.com/google/pprof v0.0.0-20240117000934-35fc243c5815/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= -github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= -github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -113,7 +112,6 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm 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/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -129,8 +127,6 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= -github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= 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= @@ -156,14 +152,14 @@ github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+ github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= -github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= -github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= +github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y= +github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/rogpeppe/clock v0.0.0-20190514195947-2896927a307a h1:3QH7VyOaaiUHNrA9Se4YQIRkDTCw1EJls9xTUCaCeRM= github.com/rogpeppe/clock v0.0.0-20190514195947-2896927a307a/go.mod h1:4r5QyqhjIWCcK8DO4KMclc5Iknq5qVBAlbYYzAbUScQ= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= @@ -182,6 +178,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/upbound/up-sdk-go v0.3.1-0.20240517133145-e5da98257888 h1:ljU7oeZk+PrjncsN2joZxO0P6DHi3NRejlhz9iu8cks= +github.com/upbound/up-sdk-go v0.3.1-0.20240517133145-e5da98257888/go.mod h1:eejYXatH60wqTy2/zbM7cjV+p53nXr0vJZWL7Uzp42Q= github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -203,13 +201,13 @@ golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o= -golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= +golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE= +golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= 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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 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= @@ -261,8 +259,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn 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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= -golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= +golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= 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= @@ -299,14 +297,14 @@ k8s.io/client-go v0.29.3 h1:R/zaZbEAxqComZ9FHeQwOh3Y1ZUs7FaHKZdQtIc2WZg= k8s.io/client-go v0.29.3/go.mod h1:tkDisCvgPfiRpxGnOORfkljmS+UrW+WtXAy2fTvXJB0= k8s.io/component-base v0.29.3 h1:Oq9/nddUxlnrCuuR2K/jp6aflVvc0uDvxMzAWxnGzAo= k8s.io/component-base v0.29.3/go.mod h1:Yuj33XXjuOk2BAaHsIGHhCKZQAgYKhqIxIjIr2UXYio= -k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= -k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= -k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= -k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= -k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= -k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/controller-runtime v0.17.0 h1:fjJQf8Ukya+VjogLO6/bNX9HE6Y2xpsO5+fyS26ur/s= -sigs.k8s.io/controller-runtime v0.17.0/go.mod h1:+MngTvIQQQhfXtwfdGw/UOQ/aIaqsYywfCINOtwMO/s= +k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= +k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20240209001042-7a0d5b415232 h1:MMq4iF9pHuAz/9dLnHwBQKEoeigXClzs3MFh/seyqtA= +k8s.io/kube-openapi v0.0.0-20240209001042-7a0d5b415232/go.mod h1:Pa1PvrP7ACSkuX6I7KYomY6cmMA0Tx86waBhDUgoKPw= +k8s.io/utils v0.0.0-20240102154912-e7106e64919e h1:eQ/4ljkx21sObifjzXwlPKpdGLrCfRziVtos3ofG/sQ= +k8s.io/utils v0.0.0-20240102154912-e7106e64919e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.17.1 h1:V1dQELMGVk46YVXXQUbTFujU7u4DQj6YUj9Rb6cuzz8= +sigs.k8s.io/controller-runtime v0.17.1/go.mod h1:+MngTvIQQQhfXtwfdGw/UOQ/aIaqsYywfCINOtwMO/s= sigs.k8s.io/controller-tools v0.14.0 h1:rnNoCC5wSXlrNoBKKzL70LNJKIQKEzT6lloG6/LF73A= sigs.k8s.io/controller-tools v0.14.0/go.mod h1:TV7uOtNNnnR72SpzhStvPkoS/U5ir0nMudrkrC4M9Sc= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= diff --git a/internal/clients/kube/kube.go b/internal/clients/kube/kube.go index 0e8ba414..93620830 100644 --- a/internal/clients/kube/kube.go +++ b/internal/clients/kube/kube.go @@ -29,16 +29,19 @@ import ( "github.com/crossplane-contrib/provider-kubernetes/apis/v1alpha1" "github.com/crossplane-contrib/provider-kubernetes/internal/clients/azure" "github.com/crossplane-contrib/provider-kubernetes/internal/clients/gke" + "github.com/crossplane-contrib/provider-kubernetes/internal/clients/upbound" ) const ( - errGetPC = "cannot get ProviderConfig" - errGetCreds = "cannot get credentials" - errCreateRestConfig = "cannot create new REST config using provider secret" - errExtractGoogleCredentials = "cannot extract Google Application Credentials" - errInjectGoogleCredentials = "cannot wrap REST client with Google Application Credentials" - errExtractAzureCredentials = "failed to extract Azure Application Credentials" - errInjectAzureCredentials = "failed to wrap REST client with Azure Application Credentials" + errGetPC = "cannot get ProviderConfig" + errGetCreds = "cannot get credentials" + errCreateRestConfig = "cannot create new REST config using provider secret" + errExtractGoogleCredentials = "cannot extract Google Application Credentials" + errInjectGoogleCredentials = "cannot wrap REST client with Google Application Credentials" + errExtractAzureCredentials = "failed to extract Azure Application Credentials" + errInjectAzureCredentials = "failed to wrap REST client with Azure Application Credentials" + errExtractUpboundCredentials = "failed to extract Upbound token" + errInjectUpboundCredentials = "failed to wrap REST client with Upbound token" ) // ClientForProvider returns the client and *rest.config for the given provider @@ -120,6 +123,21 @@ func configForProvider(ctx context.Context, local client.Client, providerConfigN return nil, errors.Wrap(err, errInjectAzureCredentials) } } + case v1alpha1.IdentityTypeUpboundToken: + switch id.Source { //nolint:exhaustive + case xpv1.CredentialsSourceInjectedIdentity: + return nil, errors.Errorf("%s is not supported as identity source for identity type %s", + xpv1.CredentialsSourceInjectedIdentity, v1alpha1.IdentityTypeUpboundToken) + default: + creds, err := resource.CommonCredentialExtractor(ctx, id.Source, local, id.CommonCredentialSelectors) + if err != nil { + return nil, errors.Wrap(err, errExtractUpboundCredentials) + } + + if err := upbound.WrapRESTConfig(ctx, rc, creds); err != nil { + return nil, errors.Wrap(err, errInjectUpboundCredentials) + } + } default: return nil, errors.Errorf("unknown identity type: %s", id.Type) } diff --git a/internal/clients/upbound/upbound.go b/internal/clients/upbound/upbound.go new file mode 100644 index 00000000..cee9283c --- /dev/null +++ b/internal/clients/upbound/upbound.go @@ -0,0 +1,86 @@ +package upbound + +import ( + "context" + "net/http" + "time" + + "github.com/pkg/errors" + "github.com/upbound/up-sdk-go" + "github.com/upbound/up-sdk-go/service/auth" + "golang.org/x/oauth2" + "k8s.io/client-go/rest" +) + +// WrapRESTConfig configures the supplied REST config to use OAuth2 access +// tokens fetched using the supplied Upbound session/robot token. +func WrapRESTConfig(_ context.Context, rc *rest.Config, token []byte, _ ...string) error { // nolint:gocyclo // mostly error handling + ex := rc.ExecProvider + if ex == nil { + return errors.New("an identity configuration was specified but the provided kubeconfig does not have execProvider section") + } + if ex.APIVersion != "client.authentication.k8s.io/v1" { + return errors.New("execProvider APIVersion is not client.authentication.k8s.io/v1") + } + if ex.Command != "up" || len(ex.Args) < 2 || ex.Args[0] != "organization" || ex.Args[1] != "token" { + return errors.New("execProvider command is not up organization token") + } + + // Read the org name, it could either be the 3rd argument and provided with `ORGANIZATION` env var. + org := "" + if len(ex.Args) > 2 { + org = ex.Args[2] + } + for _, env := range ex.Env { + if env.Name == "ORGANIZATION" { + // Env var takes precedence over the 3rd argument if both are provided. + org = env.Value + break + } + } + if org == "" { + return errors.New("organization name not provided in execProvider args or ORGANIZATION env var") + } + + rc.ExecProvider = nil + + var t *oauth2.Token + // DefaultTokenSource retrieves a token source from an injected identity. + us := &upboundTokenSource{ + client: auth.NewClient(&up.Config{ + Client: up.NewClient(func(client *up.HTTPClient) { + // TODO: This should probably be configurable. + client.BaseURL.Host = "auth.upbound.io" + }), + }), + org: org, + refreshToken: string(token), + } + ts := oauth2.ReuseTokenSource(t, us) + rc.Wrap(func(rt http.RoundTripper) http.RoundTripper { + return &oauth2.Transport{Source: ts, Base: rt} + }) + + return nil +} + +// upboundTokenSource is an oauth2.TokenSource that fetches tokens from Upbound. +type upboundTokenSource struct { + client *auth.Client + org string + refreshToken string + // Base is the base RoundTripper used to make HTTP requests. + // If nil, http.DefaultTransport is used. + Base http.RoundTripper +} + +func (s *upboundTokenSource) Token() (*oauth2.Token, error) { + resp, err := s.client.GetOrgScopedToken(context.Background(), s.org, s.refreshToken) + if err != nil { + return nil, errors.Wrap(err, "cannot get upbound org scoped token") + } + return &oauth2.Token{ + AccessToken: resp.AccessToken, + Expiry: time.Now().Add(time.Duration(resp.ExpiresIn) * time.Second), + }, nil +} diff --git a/package/crds/kubernetes.crossplane.io_providerconfigs.yaml b/package/crds/kubernetes.crossplane.io_providerconfigs.yaml index 5666d013..cc874bb7 100644 --- a/package/crds/kubernetes.crossplane.io_providerconfigs.yaml +++ b/package/crds/kubernetes.crossplane.io_providerconfigs.yaml @@ -171,6 +171,7 @@ spec: - GoogleApplicationCredentials - AzureServicePrincipalCredentials - AzureWorkloadIdentityCredentials + - UpboundToken type: string required: - source From d8d790e98425538be6f99f37223c280b833a8b72 Mon Sep 17 00:00:00 2001 From: Hasan Turken Date: Thu, 23 May 2024 13:58:22 +0300 Subject: [PATCH 2/7] Implement the ReuseSourceStore to reuse tokens accross reconcile loops Signed-off-by: Hasan Turken --- internal/clients/kube/kube.go | 11 +++-- internal/clients/token/store.go | 46 +++++++++++++++++++ internal/clients/upbound/upbound.go | 23 ++++++---- internal/controller/object/object.go | 14 ++++-- .../observedobjectcollection/reconciler.go | 6 ++- 5 files changed, 80 insertions(+), 20 deletions(-) create mode 100644 internal/clients/token/store.go diff --git a/internal/clients/kube/kube.go b/internal/clients/kube/kube.go index 93620830..8d8b2973 100644 --- a/internal/clients/kube/kube.go +++ b/internal/clients/kube/kube.go @@ -15,6 +15,7 @@ package kube import ( "context" + "github.com/crossplane-contrib/provider-kubernetes/internal/clients/token" "github.com/pkg/errors" "k8s.io/apimachinery/pkg/types" @@ -46,8 +47,8 @@ const ( // ClientForProvider returns the client and *rest.config for the given provider // config. -func ClientForProvider(ctx context.Context, inclusterClient client.Client, providerConfigName string) (client.Client, *rest.Config, error) { //nolint:gocyclo - rc, err := configForProvider(ctx, inclusterClient, providerConfigName) +func ClientForProvider(ctx context.Context, inclusterClient client.Client, store *token.ReuseSourceStore, providerConfigName string) (client.Client, *rest.Config, error) { //nolint:gocyclo + rc, err := configForProvider(ctx, inclusterClient, store, providerConfigName) if err != nil { return nil, nil, errors.Wrapf(err, "cannot get REST config for provider %q", providerConfigName) } @@ -59,7 +60,7 @@ func ClientForProvider(ctx context.Context, inclusterClient client.Client, provi } // ConfigForProvider returns the *rest.config for the given provider config. -func configForProvider(ctx context.Context, local client.Client, providerConfigName string) (*rest.Config, error) { // nolint:gocyclo +func configForProvider(ctx context.Context, local client.Client, store *token.ReuseSourceStore, providerConfigName string) (*rest.Config, error) { // nolint:gocyclo pc := &v1alpha1.ProviderConfig{} if err := local.Get(ctx, types.NamespacedName{Name: providerConfigName}, pc); err != nil { return nil, errors.Wrap(err, errGetPC) @@ -129,12 +130,12 @@ func configForProvider(ctx context.Context, local client.Client, providerConfigN return nil, errors.Errorf("%s is not supported as identity source for identity type %s", xpv1.CredentialsSourceInjectedIdentity, v1alpha1.IdentityTypeUpboundToken) default: - creds, err := resource.CommonCredentialExtractor(ctx, id.Source, local, id.CommonCredentialSelectors) + tkn, err := resource.CommonCredentialExtractor(ctx, id.Source, local, id.CommonCredentialSelectors) if err != nil { return nil, errors.Wrap(err, errExtractUpboundCredentials) } - if err := upbound.WrapRESTConfig(ctx, rc, creds); err != nil { + if err := upbound.WrapRESTConfig(ctx, rc, string(tkn), store); err != nil { return nil, errors.Wrap(err, errInjectUpboundCredentials) } } diff --git a/internal/clients/token/store.go b/internal/clients/token/store.go new file mode 100644 index 00000000..0a039887 --- /dev/null +++ b/internal/clients/token/store.go @@ -0,0 +1,46 @@ +package token + +import ( + "crypto/sha256" + "encoding/hex" + "sync" + + "golang.org/x/oauth2" +) + +type ReuseSourceStore struct { + lock sync.RWMutex + sources map[string]oauth2.TokenSource +} + +func NewReuseSourceStore() *ReuseSourceStore { + return &ReuseSourceStore{ + sources: make(map[string]oauth2.TokenSource), + } +} + +func (c *ReuseSourceStore) SourceForRefreshToken(refreshToken string, src oauth2.TokenSource) oauth2.TokenSource { + key := hashToken(refreshToken) + + c.lock.RLock() + source, exists := c.sources[key] + c.lock.RUnlock() + + if exists { + return source + } + + source = oauth2.ReuseTokenSource(nil, src) + + c.lock.Lock() + c.sources[key] = source + c.lock.Unlock() + + return source +} + +func hashToken(token string) string { + h := sha256.New() + h.Write([]byte(token)) + return hex.EncodeToString(h.Sum(nil)) +} diff --git a/internal/clients/upbound/upbound.go b/internal/clients/upbound/upbound.go index cee9283c..aa504751 100644 --- a/internal/clients/upbound/upbound.go +++ b/internal/clients/upbound/upbound.go @@ -2,6 +2,7 @@ package upbound import ( "context" + "github.com/crossplane-contrib/provider-kubernetes/internal/clients/token" "net/http" "time" @@ -12,9 +13,14 @@ import ( "k8s.io/client-go/rest" ) +const ( + // TODO: We may want to make is configurable. + authHost = "auth.upbound.io" +) + // WrapRESTConfig configures the supplied REST config to use OAuth2 access // tokens fetched using the supplied Upbound session/robot token. -func WrapRESTConfig(_ context.Context, rc *rest.Config, token []byte, _ ...string) error { // nolint:gocyclo // mostly error handling +func WrapRESTConfig(_ context.Context, rc *rest.Config, token string, store *token.ReuseSourceStore) error { // nolint:gocyclo // mostly error handling ex := rc.ExecProvider if ex == nil { return errors.New("an identity configuration was specified but the provided kubeconfig does not have execProvider section") @@ -44,21 +50,19 @@ func WrapRESTConfig(_ context.Context, rc *rest.Config, token []byte, _ ...strin rc.ExecProvider = nil - var t *oauth2.Token // DefaultTokenSource retrieves a token source from an injected identity. us := &upboundTokenSource{ client: auth.NewClient(&up.Config{ Client: up.NewClient(func(client *up.HTTPClient) { - // TODO: This should probably be configurable. - client.BaseURL.Host = "auth.upbound.io" + client.BaseURL.Host = authHost }), }), org: org, - refreshToken: string(token), + refreshToken: token, } - ts := oauth2.ReuseTokenSource(t, us) + rc.Wrap(func(rt http.RoundTripper) http.RoundTripper { - return &oauth2.Transport{Source: ts, Base: rt} + return &oauth2.Transport{Source: store.SourceForRefreshToken(token, us), Base: rt} }) return nil @@ -80,7 +84,8 @@ func (s *upboundTokenSource) Token() (*oauth2.Token, error) { return nil, errors.Wrap(err, "cannot get upbound org scoped token") } return &oauth2.Token{ - AccessToken: resp.AccessToken, - Expiry: time.Now().Add(time.Duration(resp.ExpiresIn) * time.Second), + RefreshToken: s.refreshToken, + AccessToken: resp.AccessToken, + Expiry: time.Now().Add(time.Duration(resp.ExpiresIn) * time.Second), }, nil } diff --git a/internal/controller/object/object.go b/internal/controller/object/object.go index 8f318eaa..d2d59b3f 100644 --- a/internal/controller/object/object.go +++ b/internal/controller/object/object.go @@ -20,6 +20,7 @@ import ( "context" "encoding/base64" "fmt" + "github.com/crossplane-contrib/provider-kubernetes/internal/clients/token" "math/rand" "strings" "time" @@ -130,12 +131,15 @@ func Setup(mgr ctrl.Manager, o controller.Options, sanitizeSecrets bool, pollJit managed.WithMetricRecorder(o.MetricOptions.MRMetrics), } + s := token.NewReuseSourceStore() conn := &connector{ - logger: o.Logger, - sanitizeSecrets: sanitizeSecrets, - kube: mgr.GetClient(), - usage: resource.NewProviderConfigUsageTracker(mgr.GetClient(), &apisv1alpha1.ProviderConfigUsage{}), - clientForProviderFn: kube.ClientForProvider, + logger: o.Logger, + sanitizeSecrets: sanitizeSecrets, + kube: mgr.GetClient(), + usage: resource.NewProviderConfigUsageTracker(mgr.GetClient(), &apisv1alpha1.ProviderConfigUsage{}), + clientForProviderFn: func(ctx context.Context, local client.Client, providerConfigName string) (client.Client, *rest.Config, error) { + return kube.ClientForProvider(ctx, local, s, providerConfigName) + }, } cb := ctrl.NewControllerManagedBy(mgr). diff --git a/internal/controller/observedobjectcollection/reconciler.go b/internal/controller/observedobjectcollection/reconciler.go index 825e0da7..50e3b559 100644 --- a/internal/controller/observedobjectcollection/reconciler.go +++ b/internal/controller/observedobjectcollection/reconciler.go @@ -20,6 +20,7 @@ import ( "context" "crypto/sha256" "fmt" + "github.com/crossplane-contrib/provider-kubernetes/internal/clients/token" "math/rand" "time" @@ -68,13 +69,16 @@ type Reconciler struct { func Setup(mgr ctrl.Manager, o controller.Options, pollJitter time.Duration) error { name := managed.ControllerName(v1alpha1.ObservedObjectCollectionGroupKind) + s := token.NewReuseSourceStore() r := &Reconciler{ client: mgr.GetClient(), log: o.Logger, pollInterval: func() time.Duration { return o.PollInterval + +time.Duration((rand.Float64()-0.5)*2*float64(pollJitter)) //nolint }, - clientForProvider: kube.ClientForProvider, + clientForProvider: func(ctx context.Context, inclusterClient client.Client, providerConfigName string) (client.Client, *rest.Config, error) { + return kube.ClientForProvider(ctx, inclusterClient, s, providerConfigName) + }, observedObjectName: observedObjectName, } From 8daca1ba1418589ebec4991a08f9c51b1603b799 Mon Sep 17 00:00:00 2001 From: Hasan Turken Date: Thu, 23 May 2024 14:44:09 +0300 Subject: [PATCH 3/7] Refactor kube clients out as shared package Signed-off-by: Hasan Turken --- apis/v1alpha1/types.go | 48 +---------- apis/v1alpha1/zz_generated.deepcopy.go | 53 ------------ internal/clients/clients.go | 14 ---- internal/controller/object/object.go | 34 ++++---- internal/controller/object/object_test.go | 53 +++++++----- .../observedobjectcollection/reconciler.go | 29 +++---- .../reconciler_test.go | 23 ++++- .../kube/client}/azure/azure.go | 8 +- .../kube/client}/azure/azure_test.go | 0 .../kube/client}/azure/transport.go | 0 .../kube/kube.go => pkg/kube/client/client.go | 83 ++++++++++++------- .../clients => pkg/kube/client}/gke/gke.go | 2 + .../kube/client}/token/store.go | 7 ++ .../kube/client}/upbound/upbound.go | 3 +- pkg/kube/config/config.go | 66 +++++++++++++++ pkg/kube/config/generate.go | 7 ++ pkg/kube/config/zz_generated.deepcopy.go | 76 +++++++++++++++++ 17 files changed, 301 insertions(+), 205 deletions(-) delete mode 100644 internal/clients/clients.go rename {internal/clients => pkg/kube/client}/azure/azure.go (90%) rename {internal/clients => pkg/kube/client}/azure/azure_test.go (100%) rename {internal/clients => pkg/kube/client}/azure/transport.go (100%) rename internal/clients/kube/kube.go => pkg/kube/client/client.go (66%) rename {internal/clients => pkg/kube/client}/gke/gke.go (95%) rename {internal/clients => pkg/kube/client}/token/store.go (64%) rename {internal/clients => pkg/kube/client}/upbound/upbound.go (97%) create mode 100644 pkg/kube/config/config.go create mode 100644 pkg/kube/config/generate.go create mode 100644 pkg/kube/config/zz_generated.deepcopy.go diff --git a/apis/v1alpha1/types.go b/apis/v1alpha1/types.go index 9d83e5cb..f7daab9d 100644 --- a/apis/v1alpha1/types.go +++ b/apis/v1alpha1/types.go @@ -20,52 +20,10 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" -) - -// A ProviderConfigSpec defines the desired state of a ProviderConfig. -type ProviderConfigSpec struct { - // Credentials used to connect to the Kubernetes API. Typically a - // kubeconfig file. Use InjectedIdentity for in-cluster config. - Credentials ProviderCredentials `json:"credentials"` - // Identity used to authenticate to the Kubernetes API. The identity - // credentials can be used to supplement kubeconfig 'credentials', for - // example by configuring a bearer token source such as OAuth. - // +optional - Identity *Identity `json:"identity,omitempty"` -} - -// ProviderCredentials required to authenticate. -type ProviderCredentials struct { - // Source of the provider credentials. - // +kubebuilder:validation:Enum=None;Secret;InjectedIdentity;Environment;Filesystem - Source xpv1.CredentialsSource `json:"source"` - - xpv1.CommonCredentialSelectors `json:",inline"` -} - -// IdentityType used to authenticate to the Kubernetes API. -type IdentityType string -// Supported identity types. -const ( - IdentityTypeGoogleApplicationCredentials = "GoogleApplicationCredentials" - - IdentityTypeAzureServicePrincipalCredentials = "AzureServicePrincipalCredentials" - - IdentityTypeAzureWorkloadIdentityCredentials = "AzureWorkloadIdentityCredentials" - - IdentityTypeUpboundToken = "UpboundToken" + kconfig "github.com/crossplane-contrib/provider-kubernetes/pkg/kube/config" ) -// Identity used to authenticate. -type Identity struct { - // Type of identity. - // +kubebuilder:validation:Enum=GoogleApplicationCredentials;AzureServicePrincipalCredentials;AzureWorkloadIdentityCredentials;UpboundToken - Type IdentityType `json:"type"` - - ProviderCredentials `json:",inline"` -} - // A ProviderConfigStatus reflects the observed state of a ProviderConfig. type ProviderConfigStatus struct { xpv1.ProviderConfigStatus `json:",inline"` @@ -82,8 +40,8 @@ type ProviderConfig struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec ProviderConfigSpec `json:"spec"` - Status ProviderConfigStatus `json:"status,omitempty"` + Spec kconfig.ProviderConfigSpec `json:"spec"` + Status ProviderConfigStatus `json:"status,omitempty"` } // +kubebuilder:object:root=true diff --git a/apis/v1alpha1/zz_generated.deepcopy.go b/apis/v1alpha1/zz_generated.deepcopy.go index b5182ef7..8ec99b74 100644 --- a/apis/v1alpha1/zz_generated.deepcopy.go +++ b/apis/v1alpha1/zz_generated.deepcopy.go @@ -24,22 +24,6 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Identity) DeepCopyInto(out *Identity) { - *out = *in - in.ProviderCredentials.DeepCopyInto(&out.ProviderCredentials) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Identity. -func (in *Identity) DeepCopy() *Identity { - if in == nil { - return nil - } - out := new(Identity) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ProviderConfig) DeepCopyInto(out *ProviderConfig) { *out = *in @@ -99,27 +83,6 @@ func (in *ProviderConfigList) DeepCopyObject() runtime.Object { return nil } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ProviderConfigSpec) DeepCopyInto(out *ProviderConfigSpec) { - *out = *in - in.Credentials.DeepCopyInto(&out.Credentials) - if in.Identity != nil { - in, out := &in.Identity, &out.Identity - *out = new(Identity) - (*in).DeepCopyInto(*out) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderConfigSpec. -func (in *ProviderConfigSpec) DeepCopy() *ProviderConfigSpec { - if in == nil { - return nil - } - out := new(ProviderConfigSpec) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ProviderConfigStatus) DeepCopyInto(out *ProviderConfigStatus) { *out = *in @@ -193,19 +156,3 @@ func (in *ProviderConfigUsageList) DeepCopyObject() runtime.Object { } return nil } - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ProviderCredentials) DeepCopyInto(out *ProviderCredentials) { - *out = *in - in.CommonCredentialSelectors.DeepCopyInto(&out.CommonCredentialSelectors) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderCredentials. -func (in *ProviderCredentials) DeepCopy() *ProviderCredentials { - if in == nil { - return nil - } - out := new(ProviderCredentials) - in.DeepCopyInto(out) - return out -} diff --git a/internal/clients/clients.go b/internal/clients/clients.go deleted file mode 100644 index 2a31392e..00000000 --- a/internal/clients/clients.go +++ /dev/null @@ -1,14 +0,0 @@ -/* -Copyright 2024 The Crossplane 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 clients diff --git a/internal/controller/object/object.go b/internal/controller/object/object.go index d2d59b3f..7db4d9fb 100644 --- a/internal/controller/object/object.go +++ b/internal/controller/object/object.go @@ -20,7 +20,6 @@ import ( "context" "encoding/base64" "fmt" - "github.com/crossplane-contrib/provider-kubernetes/internal/clients/token" "math/rand" "strings" "time" @@ -57,8 +56,8 @@ import ( "github.com/crossplane-contrib/provider-kubernetes/apis/object/v1alpha2" apisv1alpha1 "github.com/crossplane-contrib/provider-kubernetes/apis/v1alpha1" - "github.com/crossplane-contrib/provider-kubernetes/internal/clients/kube" "github.com/crossplane-contrib/provider-kubernetes/internal/features" + kubeclient "github.com/crossplane-contrib/provider-kubernetes/pkg/kube/client" ) type key int @@ -68,14 +67,15 @@ const ( ) const ( - errTrackPCUsage = "cannot track ProviderConfig usage" - errGetObject = "cannot get object" - errCreateObject = "cannot create object" - errApplyObject = "cannot apply object" - errDeleteObject = "cannot delete object" + errGetProviderConfig = "cannot get provider config" + errTrackPCUsage = "cannot track ProviderConfig usage" + errGetObject = "cannot get object" + errCreateObject = "cannot create object" + errApplyObject = "cannot apply object" + errDeleteObject = "cannot delete object" - errNotKubernetesObject = "managed resource is not an Object custom resource" - errNewKubernetesClient = "cannot create new Kubernetes client" + errNotKubernetesObject = "managed resource is not an Object custom resource" + errBuildKubeForProviderConfig = "cannot build kube client for provider config" errGetLastApplied = "cannot get last applied" errUnmarshalTemplate = "cannot unmarshal template" @@ -131,15 +131,12 @@ func Setup(mgr ctrl.Manager, o controller.Options, sanitizeSecrets bool, pollJit managed.WithMetricRecorder(o.MetricOptions.MRMetrics), } - s := token.NewReuseSourceStore() conn := &connector{ logger: o.Logger, sanitizeSecrets: sanitizeSecrets, kube: mgr.GetClient(), usage: resource.NewProviderConfigUsageTracker(mgr.GetClient(), &apisv1alpha1.ProviderConfigUsage{}), - clientForProviderFn: func(ctx context.Context, local client.Client, providerConfigName string) (client.Client, *rest.Config, error) { - return kube.ClientForProvider(ctx, local, s, providerConfigName) - }, + clientBuilder: kubeclient.NewIdentityAwareBuilder(mgr.GetClient()), } cb := ctrl.NewControllerManagedBy(mgr). @@ -203,13 +200,12 @@ type connector struct { kindObserver KindObserver - clientForProviderFn func(ctx context.Context, local client.Client, providerConfigName string) (client.Client, *rest.Config, error) + clientBuilder kubeclient.Builder } func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.ExternalClient, error) { //nolint:gocyclo // This method is currently a little over our complexity goal - be wary // of making it more complex. - cr, ok := mg.(*v1alpha2.Object) if !ok { return nil, errors.New(errNotKubernetesObject) @@ -219,10 +215,14 @@ func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.E return nil, errors.Wrap(err, errTrackPCUsage) } - k, rc, err := c.clientForProviderFn(ctx, c.kube, cr.GetProviderConfigReference().Name) + pc := &apisv1alpha1.ProviderConfig{} + if err := c.kube.Get(ctx, types.NamespacedName{Name: cr.GetProviderConfigReference().Name}, pc); err != nil { + return nil, errors.Wrap(err, errGetProviderConfig) + } + k, rc, err := c.clientBuilder.KubeForProviderConfig(ctx, pc.Spec) if err != nil { - return nil, errors.Wrap(err, errNewKubernetesClient) + return nil, errors.Wrap(err, errBuildKubeForProviderConfig) } return &external{ diff --git a/internal/controller/object/object_test.go b/internal/controller/object/object_test.go index 048deb4d..564518c7 100644 --- a/internal/controller/object/object_test.go +++ b/internal/controller/object/object_test.go @@ -44,6 +44,8 @@ import ( "github.com/crossplane-contrib/provider-kubernetes/apis/object/v1alpha2" kubernetesv1alpha1 "github.com/crossplane-contrib/provider-kubernetes/apis/v1alpha1" + kubeclient "github.com/crossplane-contrib/provider-kubernetes/pkg/kube/client" + kconfig "github.com/crossplane-contrib/provider-kubernetes/pkg/kube/config" ) const ( @@ -202,13 +204,13 @@ func referenceObjectWithFinalizer(val interface{}) *unstructured.Unstructured { return res } -func Test_connector_Connect(t *testing.T) { +func TestConnect(t *testing.T) { providerConfig := kubernetesv1alpha1.ProviderConfig{ ObjectMeta: metav1.ObjectMeta{Name: providerName}, - Spec: kubernetesv1alpha1.ProviderConfigSpec{ - Credentials: kubernetesv1alpha1.ProviderCredentials{}, - Identity: &kubernetesv1alpha1.Identity{ - Type: kubernetesv1alpha1.IdentityTypeGoogleApplicationCredentials, + Spec: kconfig.ProviderConfigSpec{ + Credentials: kconfig.ProviderCredentials{}, + Identity: &kconfig.Identity{ + Type: kconfig.IdentityTypeGoogleApplicationCredentials, }, }, } @@ -218,13 +220,13 @@ func Test_connector_Connect(t *testing.T) { providerConfigAzure := &kubernetesv1alpha1.ProviderConfig{ ObjectMeta: metav1.ObjectMeta{Name: providerName}, - Spec: kubernetesv1alpha1.ProviderConfigSpec{ - Credentials: kubernetesv1alpha1.ProviderCredentials{ + Spec: kconfig.ProviderConfigSpec{ + Credentials: kconfig.ProviderCredentials{ Source: xpv1.CredentialsSourceNone, }, - Identity: &kubernetesv1alpha1.Identity{ - Type: kubernetesv1alpha1.IdentityTypeAzureServicePrincipalCredentials, - ProviderCredentials: kubernetesv1alpha1.ProviderCredentials{ + Identity: &kconfig.Identity{ + Type: kconfig.IdentityTypeAzureServicePrincipalCredentials, + ProviderCredentials: kconfig.ProviderCredentials{ Source: xpv1.CredentialsSourceNone, }, }, @@ -269,9 +271,14 @@ func Test_connector_Connect(t *testing.T) { }, "Success": { args: args{ - clientForProvider: &test.MockClient{}, - usage: resource.TrackerFn(func(ctx context.Context, mg resource.Managed) error { return nil }), - mg: kubernetesObject(), + client: &test.MockClient{ + MockGet: test.NewMockGetFn(nil, func(obj client.Object) error { + *obj.(*kubernetesv1alpha1.ProviderConfig) = providerConfig + return nil + }), + }, + usage: resource.TrackerFn(func(ctx context.Context, mg resource.Managed) error { return nil }), + mg: kubernetesObject(), }, want: want{ err: nil, @@ -283,9 +290,9 @@ func Test_connector_Connect(t *testing.T) { c := &connector{ logger: logging.NewNopLogger(), kube: tc.args.client, - clientForProviderFn: func(ctx context.Context, local client.Client, providerConfigName string) (client.Client, *rest.Config, error) { + clientBuilder: kubeclient.BuilderFn(func(ctx context.Context, pc kconfig.ProviderConfigSpec) (client.Client, *rest.Config, error) { return tc.args.clientForProvider, nil, nil - }, + }), usage: tc.usage, } _, gotErr := c.Connect(context.Background(), tc.args.mg) @@ -296,7 +303,7 @@ func Test_connector_Connect(t *testing.T) { } } -func Test_helmExternal_Observe(t *testing.T) { +func TestObserve(t *testing.T) { type args struct { client resource.ClientApplicator mg resource.Managed @@ -673,7 +680,7 @@ func Test_helmExternal_Observe(t *testing.T) { } } -func Test_helmExternal_Create(t *testing.T) { +func TestCreate(t *testing.T) { type args struct { client resource.ClientApplicator mg resource.Managed @@ -781,7 +788,7 @@ func Test_helmExternal_Create(t *testing.T) { } } -func Test_helmExternal_Update(t *testing.T) { +func TestUpdate(t *testing.T) { type args struct { client resource.ClientApplicator mg resource.Managed @@ -877,7 +884,7 @@ func Test_helmExternal_Update(t *testing.T) { } } -func Test_helmExternal_Delete(t *testing.T) { +func TestDelete(t *testing.T) { type args struct { client resource.ClientApplicator mg resource.Managed @@ -970,7 +977,7 @@ func Test_helmExternal_Delete(t *testing.T) { } } -func Test_objFinalizer_AddFinalizer(t *testing.T) { +func TestAddFinalizer(t *testing.T) { type args struct { client resource.ClientApplicator mg resource.Managed @@ -1103,7 +1110,7 @@ func Test_objFinalizer_AddFinalizer(t *testing.T) { } } -func Test_objFinalizer_RemoveFinalizer(t *testing.T) { +func TestRemoveFinalizer(t *testing.T) { type args struct { client resource.ClientApplicator mg resource.Managed @@ -1264,7 +1271,7 @@ func Test_objFinalizer_RemoveFinalizer(t *testing.T) { } } -func Test_connectionDetails(t *testing.T) { +func TestConnectionDetails(t *testing.T) { mockClient := func(secretData map[string]interface{}, err error) *test.MockClient { return &test.MockClient{ MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error { @@ -1376,7 +1383,7 @@ func Test_connectionDetails(t *testing.T) { } } -func Test_updateConditionFromObserved(t *testing.T) { +func TestUpdateConditionFromObserved(t *testing.T) { type args struct { obj *v1alpha2.Object observed *unstructured.Unstructured diff --git a/internal/controller/observedobjectcollection/reconciler.go b/internal/controller/observedobjectcollection/reconciler.go index 50e3b559..0920ac41 100644 --- a/internal/controller/observedobjectcollection/reconciler.go +++ b/internal/controller/observedobjectcollection/reconciler.go @@ -20,7 +20,6 @@ import ( "context" "crypto/sha256" "fmt" - "github.com/crossplane-contrib/provider-kubernetes/internal/clients/token" "math/rand" "time" @@ -30,7 +29,6 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/sets" - "k8s.io/client-go/rest" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/predicate" @@ -45,14 +43,16 @@ import ( "github.com/crossplane-contrib/provider-kubernetes/apis/object/v1alpha2" "github.com/crossplane-contrib/provider-kubernetes/apis/observedobjectcollection/v1alpha1" - "github.com/crossplane-contrib/provider-kubernetes/internal/clients/kube" + apisv1alpha1 "github.com/crossplane-contrib/provider-kubernetes/apis/v1alpha1" + kubeclient "github.com/crossplane-contrib/provider-kubernetes/pkg/kube/client" ) const ( - errNewKubernetesClient = "cannot create new Kubernetes client" - errStatusUpdate = "cannot update status" - fieldOwner = client.FieldOwner("kubernetes.crossplane.io/observed-object-collection-controller") - membershipLabelKey = "kubernetes.crossplane.io/owned-by-collection" + errGetProviderConfig = "cannot get provider config" + errBuildKubeForProviderConfig = "cannot build kube client for provider config" + errStatusUpdate = "cannot update status" + fieldOwner = client.FieldOwner("kubernetes.crossplane.io/observed-object-collection-controller") + membershipLabelKey = "kubernetes.crossplane.io/owned-by-collection" ) // Reconciler watches for ObservedObjectCollection resources @@ -61,7 +61,7 @@ type Reconciler struct { client client.Client log logging.Logger pollInterval func() time.Duration - clientForProvider func(ctx context.Context, inclusterClient client.Client, providerConfigName string) (client.Client, *rest.Config, error) + clientBuilder kubeclient.Builder observedObjectName func(collection client.Object, matchedObject client.Object) (string, error) } @@ -69,16 +69,13 @@ type Reconciler struct { func Setup(mgr ctrl.Manager, o controller.Options, pollJitter time.Duration) error { name := managed.ControllerName(v1alpha1.ObservedObjectCollectionGroupKind) - s := token.NewReuseSourceStore() r := &Reconciler{ client: mgr.GetClient(), log: o.Logger, pollInterval: func() time.Duration { return o.PollInterval + +time.Duration((rand.Float64()-0.5)*2*float64(pollJitter)) //nolint }, - clientForProvider: func(ctx context.Context, inclusterClient client.Client, providerConfigName string) (client.Client, *rest.Config, error) { - return kube.ClientForProvider(ctx, inclusterClient, s, providerConfigName) - }, + clientBuilder: kubeclient.NewIdentityAwareBuilder(mgr.GetClient()), observedObjectName: observedObjectName, } @@ -128,10 +125,14 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ct log.Info("Reconciling") + pc := &apisv1alpha1.ProviderConfig{} + if err = r.client.Get(ctx, client.ObjectKey{Name: c.Spec.ProviderConfigReference.Name}, pc); err != nil { + return ctrl.Result{}, errors.Wrap(err, errGetProviderConfig) + } // Get client for the referenced provider config. - clusterClient, _, err := r.clientForProvider(ctx, r.client, c.Spec.ProviderConfigReference.Name) + clusterClient, _, err := r.clientBuilder.KubeForProviderConfig(ctx, pc.Spec) if err != nil { - werr := errors.Wrap(err, errNewKubernetesClient) + werr := errors.Wrap(err, errBuildKubeForProviderConfig) c.Status.SetConditions(xpv1.ReconcileError(werr)) _ = r.client.Status().Update(ctx, c) return ctrl.Result{}, werr diff --git a/internal/controller/observedobjectcollection/reconciler_test.go b/internal/controller/observedobjectcollection/reconciler_test.go index a3f898bf..e013e01b 100644 --- a/internal/controller/observedobjectcollection/reconciler_test.go +++ b/internal/controller/observedobjectcollection/reconciler_test.go @@ -43,6 +43,9 @@ import ( "github.com/crossplane-contrib/provider-kubernetes/apis/object/v1alpha2" "github.com/crossplane-contrib/provider-kubernetes/apis/observedobjectcollection/v1alpha1" + apisv1alpha1 "github.com/crossplane-contrib/provider-kubernetes/apis/v1alpha1" + kubeclient "github.com/crossplane-contrib/provider-kubernetes/pkg/kube/client" + kconfig "github.com/crossplane-contrib/provider-kubernetes/pkg/kube/config" ) func TestReconciler(t *testing.T) { @@ -91,6 +94,10 @@ func TestReconciler(t *testing.T) { args: args{ client: &test.MockClient{ MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error { + if _, ok := obj.(*apisv1alpha1.ProviderConfig); ok { + return nil + } + if key != collectionName { return fmt.Errorf("Expected %v, but got %v", collectionName, key) } @@ -185,6 +192,10 @@ func TestReconciler(t *testing.T) { args: args{ client: &test.MockClient{ MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error { + if _, ok := obj.(*apisv1alpha1.ProviderConfig); ok { + return nil + } + if key != collectionName { return fmt.Errorf("Expected %v, but got %v", collectionName, key) } @@ -282,6 +293,10 @@ func TestReconciler(t *testing.T) { args: args{ client: &test.MockClient{ MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error { + if _, ok := obj.(*apisv1alpha1.ProviderConfig); ok { + return nil + } + if key != collectionName { return fmt.Errorf("Expected %v, but got %v", collectionName, key) } @@ -322,6 +337,10 @@ func TestReconciler(t *testing.T) { args: args{ client: &test.MockClient{ MockGet: func(ctx context.Context, key client.ObjectKey, obj client.Object) error { + if _, ok := obj.(*apisv1alpha1.ProviderConfig); ok { + return nil + } + if key != collectionName { return fmt.Errorf("Expected %v, but got %v", collectionName, key) } @@ -381,9 +400,9 @@ func TestReconciler(t *testing.T) { r := &Reconciler{ client: tc.args.client, log: logging.NewNopLogger(), - clientForProvider: func(ctx context.Context, inclusterClient client.Client, providerConfigName string) (client.Client, *rest.Config, error) { + clientBuilder: kubeclient.BuilderFn(func(ctx context.Context, pc kconfig.ProviderConfigSpec) (client.Client, *rest.Config, error) { return tc.args.client, nil, nil - }, + }), observedObjectName: func(collection client.Object, matchedObject client.Object) (string, error) { return fmt.Sprintf("%s-%s", collection.GetName(), matchedObject.GetName()), nil }, diff --git a/internal/clients/azure/azure.go b/pkg/kube/client/azure/azure.go similarity index 90% rename from internal/clients/azure/azure.go rename to pkg/kube/client/azure/azure.go index c47501f5..236c1a6b 100644 --- a/internal/clients/azure/azure.go +++ b/pkg/kube/client/azure/azure.go @@ -10,7 +10,7 @@ import ( "github.com/spf13/pflag" "k8s.io/client-go/rest" - apisv1alpha1 "github.com/crossplane-contrib/provider-kubernetes/apis/v1alpha1" + kconfig "github.com/crossplane-contrib/provider-kubernetes/pkg/kube/config" ) // Credentials Secret content is a json whose keys are below. @@ -46,7 +46,7 @@ func kubeloginTokenOptionsFromRESTConfig(rc *rest.Config) (*token.Options, error // WrapRESTConfig configures the supplied REST config to use OAuth2 bearer // tokens fetched using the supplied Azure Credentials. -func WrapRESTConfig(_ context.Context, rc *rest.Config, credentials []byte, identityType apisv1alpha1.IdentityType, _ ...string) error { // nolint:gocyclo // todo: refactor +func WrapRESTConfig(_ context.Context, rc *rest.Config, credentials []byte, identityType kconfig.IdentityType, _ ...string) error { // nolint:gocyclo // todo: refactor m := map[string]string{} if err := json.Unmarshal(credentials, &m); err != nil { return err @@ -62,7 +62,7 @@ func WrapRESTConfig(_ context.Context, rc *rest.Config, credentials []byte, iden } rc.ExecProvider = nil switch identityType { - case apisv1alpha1.IdentityTypeAzureServicePrincipalCredentials: + case kconfig.IdentityTypeAzureServicePrincipalCredentials: opts.LoginMethod = token.ServicePrincipalLogin opts.ClientID = m[CredentialsKeyClientID] opts.ClientSecret = m[CredentialsKeyClientSecret] @@ -73,7 +73,7 @@ func WrapRESTConfig(_ context.Context, rc *rest.Config, credentials []byte, iden opts.ClientCertPassword = certpass } } - case apisv1alpha1.IdentityTypeAzureWorkloadIdentityCredentials: + case kconfig.IdentityTypeAzureWorkloadIdentityCredentials: opts.LoginMethod = token.WorkloadIdentityLogin opts.ClientID = m[CredentialsKeyClientID] opts.TenantID = m[CredentialsKeyTenantID] diff --git a/internal/clients/azure/azure_test.go b/pkg/kube/client/azure/azure_test.go similarity index 100% rename from internal/clients/azure/azure_test.go rename to pkg/kube/client/azure/azure_test.go diff --git a/internal/clients/azure/transport.go b/pkg/kube/client/azure/transport.go similarity index 100% rename from internal/clients/azure/transport.go rename to pkg/kube/client/azure/transport.go diff --git a/internal/clients/kube/kube.go b/pkg/kube/client/client.go similarity index 66% rename from internal/clients/kube/kube.go rename to pkg/kube/client/client.go index 8d8b2973..75da4e9f 100644 --- a/internal/clients/kube/kube.go +++ b/pkg/kube/client/client.go @@ -11,14 +11,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -package kube +package client import ( "context" - "github.com/crossplane-contrib/provider-kubernetes/internal/clients/token" "github.com/pkg/errors" - "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd/api" @@ -27,14 +25,14 @@ import ( xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" "github.com/crossplane/crossplane-runtime/pkg/resource" - "github.com/crossplane-contrib/provider-kubernetes/apis/v1alpha1" - "github.com/crossplane-contrib/provider-kubernetes/internal/clients/azure" - "github.com/crossplane-contrib/provider-kubernetes/internal/clients/gke" - "github.com/crossplane-contrib/provider-kubernetes/internal/clients/upbound" + "github.com/crossplane-contrib/provider-kubernetes/pkg/kube/client/azure" + "github.com/crossplane-contrib/provider-kubernetes/pkg/kube/client/gke" + "github.com/crossplane-contrib/provider-kubernetes/pkg/kube/client/token" + "github.com/crossplane-contrib/provider-kubernetes/pkg/kube/client/upbound" + kconfig "github.com/crossplane-contrib/provider-kubernetes/pkg/kube/config" ) const ( - errGetPC = "cannot get ProviderConfig" errGetCreds = "cannot get credentials" errCreateRestConfig = "cannot create new REST config using provider secret" errExtractGoogleCredentials = "cannot extract Google Application Credentials" @@ -45,38 +43,59 @@ const ( errInjectUpboundCredentials = "failed to wrap REST client with Upbound token" ) -// ClientForProvider returns the client and *rest.config for the given provider +// A Builder creates Kubernetes clients and REST configs for a given provider // config. -func ClientForProvider(ctx context.Context, inclusterClient client.Client, store *token.ReuseSourceStore, providerConfigName string) (client.Client, *rest.Config, error) { //nolint:gocyclo - rc, err := configForProvider(ctx, inclusterClient, store, providerConfigName) +type Builder interface { + KubeForProviderConfig(ctx context.Context, pc kconfig.ProviderConfigSpec) (client.Client, *rest.Config, error) +} + +// BuilderFn is a function that can be used as a Builder. +type BuilderFn func(ctx context.Context, pc kconfig.ProviderConfigSpec) (client.Client, *rest.Config, error) + +// KubeForProviderConfig calls the underlying function. +func (fn BuilderFn) KubeForProviderConfig(ctx context.Context, pc kconfig.ProviderConfigSpec) (client.Client, *rest.Config, error) { + return fn(ctx, pc) +} + +// IdentityAwareBuilder is a Builder that can inject identity credentials into +// the REST config of a Kubernetes client. +type IdentityAwareBuilder struct { + local client.Client + store *token.ReuseSourceStore +} + +// NewIdentityAwareBuilder returns a new IdentityAwareBuilder. +func NewIdentityAwareBuilder(local client.Client) *IdentityAwareBuilder { + return &IdentityAwareBuilder{local: local, store: token.NewReuseSourceStore()} +} + +// KubeForProviderConfig returns the kube client and *rest.config for the given +// provider config. +func (b *IdentityAwareBuilder) KubeForProviderConfig(ctx context.Context, pc kconfig.ProviderConfigSpec) (client.Client, *rest.Config, error) { + rc, err := b.restForProviderConfig(ctx, pc) if err != nil { - return nil, nil, errors.Wrapf(err, "cannot get REST config for provider %q", providerConfigName) + return nil, nil, errors.Wrapf(err, "cannot get REST config for provider") } k, err := client.New(rc, client.Options{}) if err != nil { - return nil, nil, errors.Wrapf(err, "cannot create Kubernetes client for provider %q", providerConfigName) + return nil, nil, errors.Wrapf(err, "cannot create Kubernetes client for provider") } return k, rc, nil } -// ConfigForProvider returns the *rest.config for the given provider config. -func configForProvider(ctx context.Context, local client.Client, store *token.ReuseSourceStore, providerConfigName string) (*rest.Config, error) { // nolint:gocyclo - pc := &v1alpha1.ProviderConfig{} - if err := local.Get(ctx, types.NamespacedName{Name: providerConfigName}, pc); err != nil { - return nil, errors.Wrap(err, errGetPC) - } - +// restForProviderConfig returns the *rest.config for the given provider config. +func (b *IdentityAwareBuilder) restForProviderConfig(ctx context.Context, pc kconfig.ProviderConfigSpec) (*rest.Config, error) { // nolint:gocyclo var rc *rest.Config var err error - switch cd := pc.Spec.Credentials; cd.Source { //nolint:exhaustive + switch cd := pc.Credentials; cd.Source { //nolint:exhaustive case xpv1.CredentialsSourceInjectedIdentity: rc, err = rest.InClusterConfig() if err != nil { return nil, errors.Wrap(err, errCreateRestConfig) } default: - kc, err := resource.CommonCredentialExtractor(ctx, cd.Source, local, cd.CommonCredentialSelectors) + kc, err := resource.CommonCredentialExtractor(ctx, cd.Source, b.local, cd.CommonCredentialSelectors) if err != nil { return nil, errors.Wrap(err, errGetCreds) } @@ -91,16 +110,16 @@ func configForProvider(ctx context.Context, local client.Client, store *token.Re } } - if id := pc.Spec.Identity; id != nil { + if id := pc.Identity; id != nil { switch id.Type { - case v1alpha1.IdentityTypeGoogleApplicationCredentials: + case kconfig.IdentityTypeGoogleApplicationCredentials: switch id.Source { //nolint:exhaustive case xpv1.CredentialsSourceInjectedIdentity: if err := gke.WrapRESTConfig(ctx, rc, nil, gke.DefaultScopes...); err != nil { return nil, errors.Wrap(err, errInjectGoogleCredentials) } default: - creds, err := resource.CommonCredentialExtractor(ctx, id.Source, local, id.CommonCredentialSelectors) + creds, err := resource.CommonCredentialExtractor(ctx, id.Source, b.local, id.CommonCredentialSelectors) if err != nil { return nil, errors.Wrap(err, errExtractGoogleCredentials) } @@ -109,13 +128,13 @@ func configForProvider(ctx context.Context, local client.Client, store *token.Re return nil, errors.Wrap(err, errInjectGoogleCredentials) } } - case v1alpha1.IdentityTypeAzureServicePrincipalCredentials, v1alpha1.IdentityTypeAzureWorkloadIdentityCredentials: + case kconfig.IdentityTypeAzureServicePrincipalCredentials, kconfig.IdentityTypeAzureWorkloadIdentityCredentials: switch id.Source { //nolint:exhaustive case xpv1.CredentialsSourceInjectedIdentity: return nil, errors.Errorf("%s is not supported as identity source for identity type %s", - xpv1.CredentialsSourceInjectedIdentity, v1alpha1.IdentityTypeAzureServicePrincipalCredentials) + xpv1.CredentialsSourceInjectedIdentity, kconfig.IdentityTypeAzureServicePrincipalCredentials) default: - creds, err := resource.CommonCredentialExtractor(ctx, id.Source, local, id.CommonCredentialSelectors) + creds, err := resource.CommonCredentialExtractor(ctx, id.Source, b.local, id.CommonCredentialSelectors) if err != nil { return nil, errors.Wrap(err, errExtractAzureCredentials) } @@ -124,18 +143,18 @@ func configForProvider(ctx context.Context, local client.Client, store *token.Re return nil, errors.Wrap(err, errInjectAzureCredentials) } } - case v1alpha1.IdentityTypeUpboundToken: + case kconfig.IdentityTypeUpboundToken: switch id.Source { //nolint:exhaustive case xpv1.CredentialsSourceInjectedIdentity: return nil, errors.Errorf("%s is not supported as identity source for identity type %s", - xpv1.CredentialsSourceInjectedIdentity, v1alpha1.IdentityTypeUpboundToken) + xpv1.CredentialsSourceInjectedIdentity, kconfig.IdentityTypeUpboundToken) default: - tkn, err := resource.CommonCredentialExtractor(ctx, id.Source, local, id.CommonCredentialSelectors) + tkn, err := resource.CommonCredentialExtractor(ctx, id.Source, b.local, id.CommonCredentialSelectors) if err != nil { return nil, errors.Wrap(err, errExtractUpboundCredentials) } - if err := upbound.WrapRESTConfig(ctx, rc, string(tkn), store); err != nil { + if err := upbound.WrapRESTConfig(ctx, rc, string(tkn), b.store); err != nil { return nil, errors.Wrap(err, errInjectUpboundCredentials) } } diff --git a/internal/clients/gke/gke.go b/pkg/kube/client/gke/gke.go similarity index 95% rename from internal/clients/gke/gke.go rename to pkg/kube/client/gke/gke.go index ab54dae8..c27125f0 100644 --- a/internal/clients/gke/gke.go +++ b/pkg/kube/client/gke/gke.go @@ -34,6 +34,8 @@ var DefaultScopes []string = []string{ // WrapRESTConfig configures the supplied REST config to use OAuth2 bearer // tokens fetched using the supplied Google Application Credentials. func WrapRESTConfig(ctx context.Context, rc *rest.Config, credentials []byte, scopes ...string) error { + // TODO(turkenh): Use token.ReuseSourceStore to cache token sources and + // avoid token regeneration on every reconciliation loop. var ts oauth2.TokenSource if credentials != nil { if isJSON(credentials) { diff --git a/internal/clients/token/store.go b/pkg/kube/client/token/store.go similarity index 64% rename from internal/clients/token/store.go rename to pkg/kube/client/token/store.go index 0a039887..dbf25c6c 100644 --- a/internal/clients/token/store.go +++ b/pkg/kube/client/token/store.go @@ -8,17 +8,24 @@ import ( "golang.org/x/oauth2" ) +// ReuseSourceStore is a store for reuse token sources to avoid creating new +// sources for the same refresh token in each reconciliation loop. type ReuseSourceStore struct { lock sync.RWMutex sources map[string]oauth2.TokenSource } +// NewReuseSourceStore creates a new ReuseSourceStore. func NewReuseSourceStore() *ReuseSourceStore { return &ReuseSourceStore{ sources: make(map[string]oauth2.TokenSource), } } +// SourceForRefreshToken returns a token source for the supplied refresh token. +// If a token source for the refresh token already exists, it is returned. +// Otherwise, a new reuse token source is created, stored for later access and +// returned. func (c *ReuseSourceStore) SourceForRefreshToken(refreshToken string, src oauth2.TokenSource) oauth2.TokenSource { key := hashToken(refreshToken) diff --git a/internal/clients/upbound/upbound.go b/pkg/kube/client/upbound/upbound.go similarity index 97% rename from internal/clients/upbound/upbound.go rename to pkg/kube/client/upbound/upbound.go index aa504751..a4771690 100644 --- a/internal/clients/upbound/upbound.go +++ b/pkg/kube/client/upbound/upbound.go @@ -2,7 +2,6 @@ package upbound import ( "context" - "github.com/crossplane-contrib/provider-kubernetes/internal/clients/token" "net/http" "time" @@ -11,6 +10,8 @@ import ( "github.com/upbound/up-sdk-go/service/auth" "golang.org/x/oauth2" "k8s.io/client-go/rest" + + "github.com/crossplane-contrib/provider-kubernetes/pkg/kube/client/token" ) const ( diff --git a/pkg/kube/config/config.go b/pkg/kube/config/config.go new file mode 100644 index 00000000..ebade72a --- /dev/null +++ b/pkg/kube/config/config.go @@ -0,0 +1,66 @@ +/* +Copyright 2024 The Crossplane 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 config contains API types used by Crossplane providers interacting +// with Kubernetes APIs. +// +kubebuilder:object:generate=true +package config + +import xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + +// IdentityType used to authenticate to the Kubernetes API. +// +kubebuilder:validation:Enum=GoogleApplicationCredentials;AzureServicePrincipalCredentials;AzureWorkloadIdentityCredentials;UpboundToken +type IdentityType string + +// Supported identity types. +const ( + IdentityTypeGoogleApplicationCredentials = "GoogleApplicationCredentials" + + IdentityTypeAzureServicePrincipalCredentials = "AzureServicePrincipalCredentials" + + IdentityTypeAzureWorkloadIdentityCredentials = "AzureWorkloadIdentityCredentials" + + IdentityTypeUpboundToken = "UpboundToken" +) + +// ProviderCredentials required to authenticate. +type ProviderCredentials struct { + // Source of the provider credentials. + // +kubebuilder:validation:Enum=None;Secret;InjectedIdentity;Environment;Filesystem + Source xpv1.CredentialsSource `json:"source"` + + xpv1.CommonCredentialSelectors `json:",inline"` +} + +// Identity used to authenticate. +type Identity struct { + // Type of identity. + Type IdentityType `json:"type"` + + ProviderCredentials `json:",inline"` +} + +// A ProviderConfigSpec defines the desired state of a ProviderConfig. +type ProviderConfigSpec struct { + // Credentials used to connect to the Kubernetes API. Typically a + // kubeconfig file. Use InjectedIdentity for in-cluster config. + Credentials ProviderCredentials `json:"credentials"` + // Identity used to authenticate to the Kubernetes API. The identity + // credentials can be used to supplement kubeconfig 'credentials', for + // example by configuring a bearer token source such as OAuth. + // +optional + Identity *Identity `json:"identity,omitempty"` +} diff --git a/pkg/kube/config/generate.go b/pkg/kube/config/generate.go new file mode 100644 index 00000000..6c456f69 --- /dev/null +++ b/pkg/kube/config/generate.go @@ -0,0 +1,7 @@ +//go:build generate +// +build generate + +// Generate deepcopy methodsets +//go:generate go run -tags generate sigs.k8s.io/controller-tools/cmd/controller-gen object:headerFile=../../../hack/boilerplate.go.txt paths=. + +package config diff --git a/pkg/kube/config/zz_generated.deepcopy.go b/pkg/kube/config/zz_generated.deepcopy.go new file mode 100644 index 00000000..fc2e4824 --- /dev/null +++ b/pkg/kube/config/zz_generated.deepcopy.go @@ -0,0 +1,76 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2020 The Crossplane 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. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package config + +import () + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Identity) DeepCopyInto(out *Identity) { + *out = *in + in.ProviderCredentials.DeepCopyInto(&out.ProviderCredentials) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Identity. +func (in *Identity) DeepCopy() *Identity { + if in == nil { + return nil + } + out := new(Identity) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProviderConfigSpec) DeepCopyInto(out *ProviderConfigSpec) { + *out = *in + in.Credentials.DeepCopyInto(&out.Credentials) + if in.Identity != nil { + in, out := &in.Identity, &out.Identity + *out = new(Identity) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderConfigSpec. +func (in *ProviderConfigSpec) DeepCopy() *ProviderConfigSpec { + if in == nil { + return nil + } + out := new(ProviderConfigSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProviderCredentials) DeepCopyInto(out *ProviderCredentials) { + *out = *in + in.CommonCredentialSelectors.DeepCopyInto(&out.CommonCredentialSelectors) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderCredentials. +func (in *ProviderCredentials) DeepCopy() *ProviderCredentials { + if in == nil { + return nil + } + out := new(ProviderCredentials) + in.DeepCopyInto(out) + return out +} From 7748c7f96f7d4fcb3f04a901a3dbfb3bef5acb88 Mon Sep 17 00:00:00 2001 From: Hasan Turken Date: Fri, 24 May 2024 16:57:42 +0300 Subject: [PATCH 4/7] Resolve comments for upbound identity Signed-off-by: Hasan Turken --- pkg/kube/client/upbound/upbound.go | 36 ++++++++++++++++-------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/pkg/kube/client/upbound/upbound.go b/pkg/kube/client/upbound/upbound.go index a4771690..8cd1ea12 100644 --- a/pkg/kube/client/upbound/upbound.go +++ b/pkg/kube/client/upbound/upbound.go @@ -17,11 +17,13 @@ import ( const ( // TODO: We may want to make is configurable. authHost = "auth.upbound.io" + + envVarOrganization = "ORGANIZATION" ) // WrapRESTConfig configures the supplied REST config to use OAuth2 access // tokens fetched using the supplied Upbound session/robot token. -func WrapRESTConfig(_ context.Context, rc *rest.Config, token string, store *token.ReuseSourceStore) error { // nolint:gocyclo // mostly error handling +func WrapRESTConfig(ctx context.Context, rc *rest.Config, token string, store *token.ReuseSourceStore) error { // nolint:gocyclo // mostly error handling ex := rc.ExecProvider if ex == nil { return errors.New("an identity configuration was specified but the provided kubeconfig does not have execProvider section") @@ -29,17 +31,17 @@ func WrapRESTConfig(_ context.Context, rc *rest.Config, token string, store *tok if ex.APIVersion != "client.authentication.k8s.io/v1" { return errors.New("execProvider APIVersion is not client.authentication.k8s.io/v1") } - if ex.Command != "up" || len(ex.Args) < 2 || ex.Args[0] != "organization" || ex.Args[1] != "token" { - return errors.New("execProvider command is not up organization token") + if ex.Command != "up" || len(ex.Args) < 2 || (ex.Args[0] != "org" && ex.Args[0] != "organization") || ex.Args[1] != "token" { + return errors.New("execProvider command is not up organization (org) token") } - // Read the org name, it could either be the 3rd argument and provided with `ORGANIZATION` env var. + // Read the org name, it could either be the 3rd argument or provided with `ORGANIZATION` env var. org := "" if len(ex.Args) > 2 { org = ex.Args[2] } for _, env := range ex.Env { - if env.Name == "ORGANIZATION" { + if env.Name == envVarOrganization { // Env var takes precedence over the 3rd argument if both are provided. org = env.Value break @@ -53,15 +55,18 @@ func WrapRESTConfig(_ context.Context, rc *rest.Config, token string, store *tok // DefaultTokenSource retrieves a token source from an injected identity. us := &upboundTokenSource{ + ctx: ctx, client: auth.NewClient(&up.Config{ Client: up.NewClient(func(client *up.HTTPClient) { client.BaseURL.Host = authHost }), }), - org: org, - refreshToken: token, + org: org, + staticToken: token, } + // Mutate the received REST config, rc, to use the Upbound token source at + // the transport layer. rc.Wrap(func(rt http.RoundTripper) http.RoundTripper { return &oauth2.Transport{Source: store.SourceForRefreshToken(token, us), Base: rt} }) @@ -71,22 +76,19 @@ func WrapRESTConfig(_ context.Context, rc *rest.Config, token string, store *tok // upboundTokenSource is an oauth2.TokenSource that fetches tokens from Upbound. type upboundTokenSource struct { - client *auth.Client - org string - refreshToken string - // Base is the base RoundTripper used to make HTTP requests. - // If nil, http.DefaultTransport is used. - Base http.RoundTripper + ctx context.Context + client *auth.Client + org string + staticToken string } func (s *upboundTokenSource) Token() (*oauth2.Token, error) { - resp, err := s.client.GetOrgScopedToken(context.Background(), s.org, s.refreshToken) + resp, err := s.client.GetOrgScopedToken(s.ctx, s.org, s.staticToken) if err != nil { return nil, errors.Wrap(err, "cannot get upbound org scoped token") } return &oauth2.Token{ - RefreshToken: s.refreshToken, - AccessToken: resp.AccessToken, - Expiry: time.Now().Add(time.Duration(resp.ExpiresIn) * time.Second), + AccessToken: resp.AccessToken, + Expiry: time.Now().Add(time.Duration(resp.ExpiresIn) * time.Second), }, nil } From fc15606f13e9e06c3cb279f76c806f24f9884104 Mon Sep 17 00:00:00 2001 From: Hasan Turken Date: Mon, 27 May 2024 09:39:29 +0300 Subject: [PATCH 5/7] Do not use stored context from old reconcile Signed-off-by: Hasan Turken --- pkg/kube/client/upbound/upbound.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pkg/kube/client/upbound/upbound.go b/pkg/kube/client/upbound/upbound.go index 8cd1ea12..734f8f40 100644 --- a/pkg/kube/client/upbound/upbound.go +++ b/pkg/kube/client/upbound/upbound.go @@ -23,7 +23,7 @@ const ( // WrapRESTConfig configures the supplied REST config to use OAuth2 access // tokens fetched using the supplied Upbound session/robot token. -func WrapRESTConfig(ctx context.Context, rc *rest.Config, token string, store *token.ReuseSourceStore) error { // nolint:gocyclo // mostly error handling +func WrapRESTConfig(_ context.Context, rc *rest.Config, token string, store *token.ReuseSourceStore) error { // nolint:gocyclo // mostly error handling ex := rc.ExecProvider if ex == nil { return errors.New("an identity configuration was specified but the provided kubeconfig does not have execProvider section") @@ -55,7 +55,6 @@ func WrapRESTConfig(ctx context.Context, rc *rest.Config, token string, store *t // DefaultTokenSource retrieves a token source from an injected identity. us := &upboundTokenSource{ - ctx: ctx, client: auth.NewClient(&up.Config{ Client: up.NewClient(func(client *up.HTTPClient) { client.BaseURL.Host = authHost @@ -76,14 +75,19 @@ func WrapRESTConfig(ctx context.Context, rc *rest.Config, token string, store *t // upboundTokenSource is an oauth2.TokenSource that fetches tokens from Upbound. type upboundTokenSource struct { - ctx context.Context client *auth.Client org string staticToken string } func (s *upboundTokenSource) Token() (*oauth2.Token, error) { - resp, err := s.client.GetOrgScopedToken(s.ctx, s.org, s.staticToken) + // TokenSource interface Token() method does not support context. + // https://github.com/golang/oauth2/issues/262 + // As a workaround, we create a new context with a deadline here. + ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(10*time.Second)) + defer cancel() + + resp, err := s.client.GetOrgScopedToken(ctx, s.org, s.staticToken) if err != nil { return nil, errors.Wrap(err, "cannot get upbound org scoped token") } From df40dcf765cdddb772f9f08e09adc1e5fa159d19 Mon Sep 17 00:00:00 2001 From: Hasan Turken Date: Mon, 27 May 2024 09:43:02 +0300 Subject: [PATCH 6/7] Rename identity type UpboundTokens and clean whitespaces in around token Signed-off-by: Hasan Turken --- .../provider-config-with-secret-upbound-identity.yaml | 2 +- package/crds/kubernetes.crossplane.io_providerconfigs.yaml | 2 +- pkg/kube/client/client.go | 7 ++++--- pkg/kube/config/config.go | 4 ++-- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/examples/provider/provider-config-with-secret-upbound-identity.yaml b/examples/provider/provider-config-with-secret-upbound-identity.yaml index 0b8d406c..b220b7a5 100644 --- a/examples/provider/provider-config-with-secret-upbound-identity.yaml +++ b/examples/provider/provider-config-with-secret-upbound-identity.yaml @@ -10,7 +10,7 @@ spec: name: cluster-config key: kubeconfig identity: - type: UpboundToken + type: UpboundTokens source: Secret secretRef: name: upbound-credentials diff --git a/package/crds/kubernetes.crossplane.io_providerconfigs.yaml b/package/crds/kubernetes.crossplane.io_providerconfigs.yaml index cc874bb7..a8ab423c 100644 --- a/package/crds/kubernetes.crossplane.io_providerconfigs.yaml +++ b/package/crds/kubernetes.crossplane.io_providerconfigs.yaml @@ -171,7 +171,7 @@ spec: - GoogleApplicationCredentials - AzureServicePrincipalCredentials - AzureWorkloadIdentityCredentials - - UpboundToken + - UpboundTokens type: string required: - source diff --git a/pkg/kube/client/client.go b/pkg/kube/client/client.go index 75da4e9f..f88589f9 100644 --- a/pkg/kube/client/client.go +++ b/pkg/kube/client/client.go @@ -15,6 +15,7 @@ package client import ( "context" + "strings" "github.com/pkg/errors" "k8s.io/client-go/rest" @@ -143,18 +144,18 @@ func (b *IdentityAwareBuilder) restForProviderConfig(ctx context.Context, pc kco return nil, errors.Wrap(err, errInjectAzureCredentials) } } - case kconfig.IdentityTypeUpboundToken: + case kconfig.IdentityTypeUpboundTokens: switch id.Source { //nolint:exhaustive case xpv1.CredentialsSourceInjectedIdentity: return nil, errors.Errorf("%s is not supported as identity source for identity type %s", - xpv1.CredentialsSourceInjectedIdentity, kconfig.IdentityTypeUpboundToken) + xpv1.CredentialsSourceInjectedIdentity, kconfig.IdentityTypeUpboundTokens) default: tkn, err := resource.CommonCredentialExtractor(ctx, id.Source, b.local, id.CommonCredentialSelectors) if err != nil { return nil, errors.Wrap(err, errExtractUpboundCredentials) } - if err := upbound.WrapRESTConfig(ctx, rc, string(tkn), b.store); err != nil { + if err := upbound.WrapRESTConfig(ctx, rc, strings.TrimSpace(string(tkn)), b.store); err != nil { return nil, errors.Wrap(err, errInjectUpboundCredentials) } } diff --git a/pkg/kube/config/config.go b/pkg/kube/config/config.go index ebade72a..fbe29899 100644 --- a/pkg/kube/config/config.go +++ b/pkg/kube/config/config.go @@ -22,7 +22,7 @@ package config import xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" // IdentityType used to authenticate to the Kubernetes API. -// +kubebuilder:validation:Enum=GoogleApplicationCredentials;AzureServicePrincipalCredentials;AzureWorkloadIdentityCredentials;UpboundToken +// +kubebuilder:validation:Enum=GoogleApplicationCredentials;AzureServicePrincipalCredentials;AzureWorkloadIdentityCredentials;UpboundTokens type IdentityType string // Supported identity types. @@ -33,7 +33,7 @@ const ( IdentityTypeAzureWorkloadIdentityCredentials = "AzureWorkloadIdentityCredentials" - IdentityTypeUpboundToken = "UpboundToken" + IdentityTypeUpboundTokens = "UpboundTokens" ) // ProviderCredentials required to authenticate. From c9bfe04b80121e6ba37355d9f85ddafe5a51d363 Mon Sep 17 00:00:00 2001 From: Hasan Turken Date: Tue, 28 May 2024 13:40:12 +0300 Subject: [PATCH 7/7] Add note on why we trim upbound token Signed-off-by: Hasan Turken --- pkg/kube/client/client.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/kube/client/client.go b/pkg/kube/client/client.go index f88589f9..2d5cd5ad 100644 --- a/pkg/kube/client/client.go +++ b/pkg/kube/client/client.go @@ -150,12 +150,15 @@ func (b *IdentityAwareBuilder) restForProviderConfig(ctx context.Context, pc kco return nil, errors.Errorf("%s is not supported as identity source for identity type %s", xpv1.CredentialsSourceInjectedIdentity, kconfig.IdentityTypeUpboundTokens) default: - tkn, err := resource.CommonCredentialExtractor(ctx, id.Source, b.local, id.CommonCredentialSelectors) + staticToken, err := resource.CommonCredentialExtractor(ctx, id.Source, b.local, id.CommonCredentialSelectors) if err != nil { return nil, errors.Wrap(err, errExtractUpboundCredentials) } - if err := upbound.WrapRESTConfig(ctx, rc, strings.TrimSpace(string(tkn)), b.store); err != nil { + // We trim the token to remove any leading/trailing whitespace + // which may have been added especially when stringData field + // is used while creating the secret. + if err := upbound.WrapRESTConfig(ctx, rc, strings.TrimSpace(string(staticToken)), b.store); err != nil { return nil, errors.Wrap(err, errInjectUpboundCredentials) } }