123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220 |
- /*
- Copyright 2018 The Kubernetes Authors.
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- package controller
- import (
- "context"
- "fmt"
- "net/http"
- "sync"
- "time"
- "golang.org/x/oauth2"
- v1authenticationapi "k8s.io/api/authentication/v1"
- metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
- "k8s.io/apimachinery/pkg/util/clock"
- "k8s.io/apimachinery/pkg/util/wait"
- apiserverserviceaccount "k8s.io/apiserver/pkg/authentication/serviceaccount"
- clientset "k8s.io/client-go/kubernetes"
- v1core "k8s.io/client-go/kubernetes/typed/core/v1"
- restclient "k8s.io/client-go/rest"
- "k8s.io/client-go/transport"
- "k8s.io/klog"
- utilpointer "k8s.io/utils/pointer"
- )
- var (
- // defaultExpirationSeconds defines the duration of a TokenRequest in seconds.
- defaultExpirationSeconds = int64(3600)
- // defaultLeewayPercent defines the percentage of expiration left before the client trigger a token rotation.
- // range[0, 100]
- defaultLeewayPercent = 20
- )
- type DynamicControllerClientBuilder struct {
- // ClientConfig is a skeleton config to clone and use as the basis for each controller client
- ClientConfig *restclient.Config
- // CoreClient is used to provision service accounts if needed and watch for their associated tokens
- // to construct a controller client
- CoreClient v1core.CoreV1Interface
- // Namespace is the namespace used to host the service accounts that will back the
- // controllers. It must be highly privileged namespace which normal users cannot inspect.
- Namespace string
- // roundTripperFuncMap is a cache stores the corresponding roundtripper func for each
- // service account
- roundTripperFuncMap map[string]func(http.RoundTripper) http.RoundTripper
- // expirationSeconds defines the token expiration seconds
- expirationSeconds int64
- // leewayPercent defines the percentage of expiration left before the client trigger a token rotation.
- leewayPercent int
- mutex sync.Mutex
- clock clock.Clock
- }
- func NewDynamicClientBuilder(clientConfig *restclient.Config, coreClient v1core.CoreV1Interface, ns string) ControllerClientBuilder {
- builder := &DynamicControllerClientBuilder{
- ClientConfig: clientConfig,
- CoreClient: coreClient,
- Namespace: ns,
- roundTripperFuncMap: map[string]func(http.RoundTripper) http.RoundTripper{},
- expirationSeconds: defaultExpirationSeconds,
- leewayPercent: defaultLeewayPercent,
- clock: clock.RealClock{},
- }
- return builder
- }
- // this function only for test purpose, don't call it
- func NewTestDynamicClientBuilder(clientConfig *restclient.Config, coreClient v1core.CoreV1Interface, ns string, expirationSeconds int64, leewayPercent int) ControllerClientBuilder {
- builder := &DynamicControllerClientBuilder{
- ClientConfig: clientConfig,
- CoreClient: coreClient,
- Namespace: ns,
- roundTripperFuncMap: map[string]func(http.RoundTripper) http.RoundTripper{},
- expirationSeconds: expirationSeconds,
- leewayPercent: leewayPercent,
- clock: clock.RealClock{},
- }
- return builder
- }
- func (t *DynamicControllerClientBuilder) Config(saName string) (*restclient.Config, error) {
- _, err := getOrCreateServiceAccount(t.CoreClient, t.Namespace, saName)
- if err != nil {
- return nil, err
- }
- configCopy := constructClient(t.Namespace, saName, t.ClientConfig)
- t.mutex.Lock()
- defer t.mutex.Unlock()
- rt, ok := t.roundTripperFuncMap[saName]
- if ok {
- configCopy.WrapTransport = rt
- } else {
- cachedTokenSource := transport.NewCachedTokenSource(&tokenSourceImpl{
- namespace: t.Namespace,
- serviceAccountName: saName,
- coreClient: t.CoreClient,
- expirationSeconds: t.expirationSeconds,
- leewayPercent: t.leewayPercent,
- })
- configCopy.WrapTransport = transport.TokenSourceWrapTransport(cachedTokenSource)
- t.roundTripperFuncMap[saName] = configCopy.WrapTransport
- }
- return &configCopy, nil
- }
- func (t *DynamicControllerClientBuilder) ConfigOrDie(name string) *restclient.Config {
- clientConfig, err := t.Config(name)
- if err != nil {
- klog.Fatal(err)
- }
- return clientConfig
- }
- func (t *DynamicControllerClientBuilder) Client(name string) (clientset.Interface, error) {
- clientConfig, err := t.Config(name)
- if err != nil {
- return nil, err
- }
- return clientset.NewForConfig(clientConfig)
- }
- func (t *DynamicControllerClientBuilder) ClientOrDie(name string) clientset.Interface {
- client, err := t.Client(name)
- if err != nil {
- klog.Fatal(err)
- }
- return client
- }
- type tokenSourceImpl struct {
- namespace string
- serviceAccountName string
- coreClient v1core.CoreV1Interface
- expirationSeconds int64
- leewayPercent int
- }
- func (ts *tokenSourceImpl) Token() (*oauth2.Token, error) {
- var retTokenRequest *v1authenticationapi.TokenRequest
- backoff := wait.Backoff{
- Duration: 500 * time.Millisecond,
- Factor: 2, // double the timeout for every failure
- Steps: 4,
- }
- if err := wait.ExponentialBackoff(backoff, func() (bool, error) {
- if _, inErr := getOrCreateServiceAccount(ts.coreClient, ts.namespace, ts.serviceAccountName); inErr != nil {
- klog.Warningf("get or create service account failed: %v", inErr)
- return false, nil
- }
- tr, inErr := ts.coreClient.ServiceAccounts(ts.namespace).CreateToken(context.TODO(), ts.serviceAccountName, &v1authenticationapi.TokenRequest{
- Spec: v1authenticationapi.TokenRequestSpec{
- ExpirationSeconds: utilpointer.Int64Ptr(ts.expirationSeconds),
- },
- }, metav1.CreateOptions{})
- if inErr != nil {
- klog.Warningf("get token failed: %v", inErr)
- return false, nil
- }
- retTokenRequest = tr
- return true, nil
- }); err != nil {
- return nil, fmt.Errorf("failed to get token for %s/%s: %v", ts.namespace, ts.serviceAccountName, err)
- }
- if retTokenRequest.Spec.ExpirationSeconds == nil {
- return nil, fmt.Errorf("nil pointer of expiration in token request")
- }
- lifetime := retTokenRequest.Status.ExpirationTimestamp.Time.Sub(time.Now())
- if lifetime < time.Minute*10 {
- // possible clock skew issue, pin to minimum token lifetime
- lifetime = time.Minute * 10
- }
- leeway := time.Duration(int64(lifetime) * int64(ts.leewayPercent) / 100)
- expiry := time.Now().Add(lifetime).Add(-1 * leeway)
- return &oauth2.Token{
- AccessToken: retTokenRequest.Status.Token,
- TokenType: "Bearer",
- Expiry: expiry,
- }, nil
- }
- func constructClient(saNamespace, saName string, config *restclient.Config) restclient.Config {
- username := apiserverserviceaccount.MakeUsername(saNamespace, saName)
- ret := *restclient.AnonymousClientConfig(config)
- restclient.AddUserAgent(&ret, username)
- return ret
- }
|