client_builder_dynamic.go 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. /*
  2. Copyright 2018 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package controller
  14. import (
  15. "fmt"
  16. "net/http"
  17. "sync"
  18. "time"
  19. "golang.org/x/oauth2"
  20. v1authenticationapi "k8s.io/api/authentication/v1"
  21. "k8s.io/apimachinery/pkg/util/clock"
  22. "k8s.io/apimachinery/pkg/util/wait"
  23. apiserverserviceaccount "k8s.io/apiserver/pkg/authentication/serviceaccount"
  24. clientset "k8s.io/client-go/kubernetes"
  25. v1core "k8s.io/client-go/kubernetes/typed/core/v1"
  26. restclient "k8s.io/client-go/rest"
  27. "k8s.io/client-go/transport"
  28. "k8s.io/klog"
  29. utilpointer "k8s.io/utils/pointer"
  30. )
  31. var (
  32. // defaultExpirationSeconds defines the duration of a TokenRequest in seconds.
  33. defaultExpirationSeconds = int64(3600)
  34. // defaultLeewayPercent defines the percentage of expiration left before the client trigger a token rotation.
  35. // range[0, 100]
  36. defaultLeewayPercent = 20
  37. )
  38. type DynamicControllerClientBuilder struct {
  39. // ClientConfig is a skeleton config to clone and use as the basis for each controller client
  40. ClientConfig *restclient.Config
  41. // CoreClient is used to provision service accounts if needed and watch for their associated tokens
  42. // to construct a controller client
  43. CoreClient v1core.CoreV1Interface
  44. // Namespace is the namespace used to host the service accounts that will back the
  45. // controllers. It must be highly privileged namespace which normal users cannot inspect.
  46. Namespace string
  47. // roundTripperFuncMap is a cache stores the corresponding roundtripper func for each
  48. // service account
  49. roundTripperFuncMap map[string]func(http.RoundTripper) http.RoundTripper
  50. // expirationSeconds defines the token expiration seconds
  51. expirationSeconds int64
  52. // leewayPercent defines the percentage of expiration left before the client trigger a token rotation.
  53. leewayPercent int
  54. mutex sync.Mutex
  55. clock clock.Clock
  56. }
  57. func NewDynamicClientBuilder(clientConfig *restclient.Config, coreClient v1core.CoreV1Interface, ns string) ControllerClientBuilder {
  58. builder := &DynamicControllerClientBuilder{
  59. ClientConfig: clientConfig,
  60. CoreClient: coreClient,
  61. Namespace: ns,
  62. roundTripperFuncMap: map[string]func(http.RoundTripper) http.RoundTripper{},
  63. expirationSeconds: defaultExpirationSeconds,
  64. leewayPercent: defaultLeewayPercent,
  65. clock: clock.RealClock{},
  66. }
  67. return builder
  68. }
  69. // this function only for test purpose, don't call it
  70. func NewTestDynamicClientBuilder(clientConfig *restclient.Config, coreClient v1core.CoreV1Interface, ns string, expirationSeconds int64, leewayPercent int) ControllerClientBuilder {
  71. builder := &DynamicControllerClientBuilder{
  72. ClientConfig: clientConfig,
  73. CoreClient: coreClient,
  74. Namespace: ns,
  75. roundTripperFuncMap: map[string]func(http.RoundTripper) http.RoundTripper{},
  76. expirationSeconds: expirationSeconds,
  77. leewayPercent: leewayPercent,
  78. clock: clock.RealClock{},
  79. }
  80. return builder
  81. }
  82. func (t *DynamicControllerClientBuilder) Config(saName string) (*restclient.Config, error) {
  83. _, err := getOrCreateServiceAccount(t.CoreClient, t.Namespace, saName)
  84. if err != nil {
  85. return nil, err
  86. }
  87. configCopy := constructClient(t.Namespace, saName, t.ClientConfig)
  88. t.mutex.Lock()
  89. defer t.mutex.Unlock()
  90. rt, ok := t.roundTripperFuncMap[saName]
  91. if ok {
  92. configCopy.WrapTransport = rt
  93. } else {
  94. cachedTokenSource := transport.NewCachedTokenSource(&tokenSourceImpl{
  95. namespace: t.Namespace,
  96. serviceAccountName: saName,
  97. coreClient: t.CoreClient,
  98. expirationSeconds: t.expirationSeconds,
  99. leewayPercent: t.leewayPercent,
  100. })
  101. configCopy.WrapTransport = transport.TokenSourceWrapTransport(cachedTokenSource)
  102. t.roundTripperFuncMap[saName] = configCopy.WrapTransport
  103. }
  104. return &configCopy, nil
  105. }
  106. func (t *DynamicControllerClientBuilder) ConfigOrDie(name string) *restclient.Config {
  107. clientConfig, err := t.Config(name)
  108. if err != nil {
  109. klog.Fatal(err)
  110. }
  111. return clientConfig
  112. }
  113. func (t *DynamicControllerClientBuilder) Client(name string) (clientset.Interface, error) {
  114. clientConfig, err := t.Config(name)
  115. if err != nil {
  116. return nil, err
  117. }
  118. return clientset.NewForConfig(clientConfig)
  119. }
  120. func (t *DynamicControllerClientBuilder) ClientOrDie(name string) clientset.Interface {
  121. client, err := t.Client(name)
  122. if err != nil {
  123. klog.Fatal(err)
  124. }
  125. return client
  126. }
  127. type tokenSourceImpl struct {
  128. namespace string
  129. serviceAccountName string
  130. coreClient v1core.CoreV1Interface
  131. expirationSeconds int64
  132. leewayPercent int
  133. }
  134. func (ts *tokenSourceImpl) Token() (*oauth2.Token, error) {
  135. var retTokenRequest *v1authenticationapi.TokenRequest
  136. backoff := wait.Backoff{
  137. Duration: 500 * time.Millisecond,
  138. Factor: 2, // double the timeout for every failure
  139. Steps: 4,
  140. }
  141. if err := wait.ExponentialBackoff(backoff, func() (bool, error) {
  142. if _, inErr := getOrCreateServiceAccount(ts.coreClient, ts.namespace, ts.serviceAccountName); inErr != nil {
  143. klog.Warningf("get or create service account failed: %v", inErr)
  144. return false, nil
  145. }
  146. tr, inErr := ts.coreClient.ServiceAccounts(ts.namespace).CreateToken(ts.serviceAccountName, &v1authenticationapi.TokenRequest{
  147. Spec: v1authenticationapi.TokenRequestSpec{
  148. ExpirationSeconds: utilpointer.Int64Ptr(ts.expirationSeconds),
  149. },
  150. })
  151. if inErr != nil {
  152. klog.Warningf("get token failed: %v", inErr)
  153. return false, nil
  154. }
  155. retTokenRequest = tr
  156. return true, nil
  157. }); err != nil {
  158. return nil, fmt.Errorf("failed to get token for %s/%s: %v", ts.namespace, ts.serviceAccountName, err)
  159. }
  160. if retTokenRequest.Spec.ExpirationSeconds == nil {
  161. return nil, fmt.Errorf("nil pointer of expiration in token request")
  162. }
  163. lifetime := retTokenRequest.Status.ExpirationTimestamp.Time.Sub(time.Now())
  164. if lifetime < time.Minute*10 {
  165. // possible clock skew issue, pin to minimum token lifetime
  166. lifetime = time.Minute * 10
  167. }
  168. leeway := time.Duration(int64(lifetime) * int64(ts.leewayPercent) / 100)
  169. expiry := time.Now().Add(lifetime).Add(-1 * leeway)
  170. return &oauth2.Token{
  171. AccessToken: retTokenRequest.Status.Token,
  172. TokenType: "Bearer",
  173. Expiry: expiry,
  174. }, nil
  175. }
  176. func constructClient(saNamespace, saName string, config *restclient.Config) restclient.Config {
  177. username := apiserverserviceaccount.MakeUsername(saNamespace, saName)
  178. ret := *restclient.AnonymousClientConfig(config)
  179. restclient.AddUserAgent(&ret, username)
  180. return ret
  181. }