diff --git a/pkg/controller/volume/attachdetach/BUILD b/pkg/controller/volume/attachdetach/BUILD index 4d5908494ed..f30b4b8d4a3 100644 --- a/pkg/controller/volume/attachdetach/BUILD +++ b/pkg/controller/volume/attachdetach/BUILD @@ -25,6 +25,7 @@ go_library( "//pkg/volume/util/operationexecutor:go_default_library", "//pkg/volume/util/volumepathhandler:go_default_library", "//vendor/github.com/golang/glog:go_default_library", + "//vendor/k8s.io/api/authentication/v1:go_default_library", "//vendor/k8s.io/api/core/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/labels:go_default_library", "//vendor/k8s.io/apimachinery/pkg/types:go_default_library", diff --git a/pkg/controller/volume/attachdetach/attach_detach_controller.go b/pkg/controller/volume/attachdetach/attach_detach_controller.go index c7397f57535..805a304952f 100644 --- a/pkg/controller/volume/attachdetach/attach_detach_controller.go +++ b/pkg/controller/volume/attachdetach/attach_detach_controller.go @@ -24,6 +24,7 @@ import ( "time" "github.com/golang/glog" + authenticationv1 "k8s.io/api/authentication/v1" "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/types" @@ -586,6 +587,12 @@ func (adc *attachDetachController) GetConfigMapFunc() func(namespace, name strin } } +func (adc *attachDetachController) GetServiceAccountTokenFunc() func(_, _ string, _ *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) { + return func(_, _ string, _ *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) { + return nil, fmt.Errorf("GetServiceAccountToken unsupported in attachDetachController") + } +} + func (adc *attachDetachController) GetExec(pluginName string) mount.Exec { return mount.NewOsExec() } diff --git a/pkg/controller/volume/expand/BUILD b/pkg/controller/volume/expand/BUILD index 5a067da6a44..69c1101426d 100644 --- a/pkg/controller/volume/expand/BUILD +++ b/pkg/controller/volume/expand/BUILD @@ -25,6 +25,7 @@ go_library( "//pkg/volume/util/operationexecutor:go_default_library", "//pkg/volume/util/volumepathhandler:go_default_library", "//vendor/github.com/golang/glog:go_default_library", + "//vendor/k8s.io/api/authentication/v1:go_default_library", "//vendor/k8s.io/api/core/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/labels:go_default_library", diff --git a/pkg/controller/volume/expand/expand_controller.go b/pkg/controller/volume/expand/expand_controller.go index caceb853ef1..9a8e8d37978 100644 --- a/pkg/controller/volume/expand/expand_controller.go +++ b/pkg/controller/volume/expand/expand_controller.go @@ -26,6 +26,7 @@ import ( "github.com/golang/glog" + authenticationv1 "k8s.io/api/authentication/v1" "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/runtime" @@ -295,6 +296,12 @@ func (expc *expandController) GetConfigMapFunc() func(namespace, name string) (* } } +func (expc *expandController) GetServiceAccountTokenFunc() func(_, _ string, _ *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) { + return func(_, _ string, _ *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) { + return nil, fmt.Errorf("GetServiceAccountToken unsupported in expandController") + } +} + func (expc *expandController) GetNodeLabels() (map[string]string, error) { return nil, fmt.Errorf("GetNodeLabels unsupported in expandController") } diff --git a/pkg/controller/volume/persistentvolume/BUILD b/pkg/controller/volume/persistentvolume/BUILD index 5367ef78fab..ec57338d75d 100644 --- a/pkg/controller/volume/persistentvolume/BUILD +++ b/pkg/controller/volume/persistentvolume/BUILD @@ -34,6 +34,7 @@ go_library( "//pkg/volume/util:go_default_library", "//pkg/volume/util/recyclerclient:go_default_library", "//vendor/github.com/golang/glog:go_default_library", + "//vendor/k8s.io/api/authentication/v1:go_default_library", "//vendor/k8s.io/api/core/v1:go_default_library", "//vendor/k8s.io/api/storage/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library", diff --git a/pkg/controller/volume/persistentvolume/volume_host.go b/pkg/controller/volume/persistentvolume/volume_host.go index 14878c12e92..7fa54790c62 100644 --- a/pkg/controller/volume/persistentvolume/volume_host.go +++ b/pkg/controller/volume/persistentvolume/volume_host.go @@ -20,6 +20,7 @@ import ( "fmt" "net" + authenticationv1 "k8s.io/api/authentication/v1" "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" clientset "k8s.io/client-go/kubernetes" @@ -94,18 +95,24 @@ func (ctrl *PersistentVolumeController) GetNodeAllocatable() (v1.ResourceList, e return v1.ResourceList{}, nil } -func (adc *PersistentVolumeController) GetSecretFunc() func(namespace, name string) (*v1.Secret, error) { +func (ctrl *PersistentVolumeController) GetSecretFunc() func(namespace, name string) (*v1.Secret, error) { return func(_, _ string) (*v1.Secret, error) { return nil, fmt.Errorf("GetSecret unsupported in PersistentVolumeController") } } -func (adc *PersistentVolumeController) GetConfigMapFunc() func(namespace, name string) (*v1.ConfigMap, error) { +func (ctrl *PersistentVolumeController) GetConfigMapFunc() func(namespace, name string) (*v1.ConfigMap, error) { return func(_, _ string) (*v1.ConfigMap, error) { return nil, fmt.Errorf("GetConfigMap unsupported in PersistentVolumeController") } } +func (ctrl *PersistentVolumeController) GetServiceAccountTokenFunc() func(_, _ string, _ *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) { + return func(_, _ string, _ *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) { + return nil, fmt.Errorf("GetServiceAccountToken unsupported in PersistentVolumeController") + } +} + func (adc *PersistentVolumeController) GetExec(pluginName string) mount.Exec { return mount.NewOsExec() } diff --git a/pkg/kubelet/BUILD b/pkg/kubelet/BUILD index 305cfd970ec..ca892efcda5 100644 --- a/pkg/kubelet/BUILD +++ b/pkg/kubelet/BUILD @@ -80,6 +80,7 @@ go_library( "//pkg/kubelet/stats:go_default_library", "//pkg/kubelet/status:go_default_library", "//pkg/kubelet/sysctl:go_default_library", + "//pkg/kubelet/token:go_default_library", "//pkg/kubelet/types:go_default_library", "//pkg/kubelet/util:go_default_library", "//pkg/kubelet/util/format:go_default_library", @@ -113,6 +114,7 @@ go_library( "//vendor/github.com/google/cadvisor/events:go_default_library", "//vendor/github.com/google/cadvisor/info/v1:go_default_library", "//vendor/github.com/google/cadvisor/info/v2:go_default_library", + "//vendor/k8s.io/api/authentication/v1:go_default_library", "//vendor/k8s.io/api/core/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/resource:go_default_library", @@ -191,6 +193,7 @@ go_test( "//pkg/kubelet/stats:go_default_library", "//pkg/kubelet/status:go_default_library", "//pkg/kubelet/status/testing:go_default_library", + "//pkg/kubelet/token:go_default_library", "//pkg/kubelet/types:go_default_library", "//pkg/kubelet/util/queue:go_default_library", "//pkg/kubelet/util/sliceutils:go_default_library", @@ -282,6 +285,7 @@ filegroup( "//pkg/kubelet/stats:all-srcs", "//pkg/kubelet/status:all-srcs", "//pkg/kubelet/sysctl:all-srcs", + "//pkg/kubelet/token:all-srcs", "//pkg/kubelet/types:all-srcs", "//pkg/kubelet/util:all-srcs", "//pkg/kubelet/volumemanager:all-srcs", diff --git a/pkg/kubelet/kubelet.go b/pkg/kubelet/kubelet.go index be5d5fd1fd9..0207a88eddc 100644 --- a/pkg/kubelet/kubelet.go +++ b/pkg/kubelet/kubelet.go @@ -90,6 +90,7 @@ import ( "k8s.io/kubernetes/pkg/kubelet/stats" "k8s.io/kubernetes/pkg/kubelet/status" "k8s.io/kubernetes/pkg/kubelet/sysctl" + "k8s.io/kubernetes/pkg/kubelet/token" kubetypes "k8s.io/kubernetes/pkg/kubelet/types" "k8s.io/kubernetes/pkg/kubelet/util/format" "k8s.io/kubernetes/pkg/kubelet/util/manager" @@ -779,8 +780,10 @@ func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration, containerRefManager, kubeDeps.Recorder) + tokenManager := token.NewManager(kubeDeps.KubeClient.CoreV1()) + klet.volumePluginMgr, err = - NewInitializedVolumePluginMgr(klet, secretManager, configMapManager, kubeDeps.VolumePlugins, kubeDeps.DynamicPluginProber) + NewInitializedVolumePluginMgr(klet, secretManager, configMapManager, tokenManager, kubeDeps.VolumePlugins, kubeDeps.DynamicPluginProber) if err != nil { return nil, err } diff --git a/pkg/kubelet/kubelet_test.go b/pkg/kubelet/kubelet_test.go index 3876f245152..f49d86eed1c 100644 --- a/pkg/kubelet/kubelet_test.go +++ b/pkg/kubelet/kubelet_test.go @@ -61,6 +61,7 @@ import ( "k8s.io/kubernetes/pkg/kubelet/stats" "k8s.io/kubernetes/pkg/kubelet/status" statustest "k8s.io/kubernetes/pkg/kubelet/status/testing" + "k8s.io/kubernetes/pkg/kubelet/token" kubetypes "k8s.io/kubernetes/pkg/kubelet/types" "k8s.io/kubernetes/pkg/kubelet/util/queue" kubeletvolume "k8s.io/kubernetes/pkg/kubelet/volumemanager" @@ -325,7 +326,7 @@ func newTestKubeletWithImageList( var prober volume.DynamicPluginProber = nil // TODO (#51147) inject mock kubelet.volumePluginMgr, err = - NewInitializedVolumePluginMgr(kubelet, kubelet.secretManager, kubelet.configMapManager, allPlugins, prober) + NewInitializedVolumePluginMgr(kubelet, kubelet.secretManager, kubelet.configMapManager, token.NewManager(kubelet.kubeClient.CoreV1()), allPlugins, prober) require.NoError(t, err, "Failed to initialize VolumePluginMgr") kubelet.mounter = &mount.FakeMounter{} diff --git a/pkg/kubelet/runonce_test.go b/pkg/kubelet/runonce_test.go index d8e943c1f75..6047d664bc0 100644 --- a/pkg/kubelet/runonce_test.go +++ b/pkg/kubelet/runonce_test.go @@ -90,7 +90,7 @@ func TestRunOnce(t *testing.T) { plug := &volumetest.FakeVolumePlugin{PluginName: "fake", Host: nil} kb.volumePluginMgr, err = - NewInitializedVolumePluginMgr(kb, fakeSecretManager, fakeConfigMapManager, []volume.VolumePlugin{plug}, nil /* prober */) + NewInitializedVolumePluginMgr(kb, fakeSecretManager, fakeConfigMapManager, nil, []volume.VolumePlugin{plug}, nil /* prober */) if err != nil { t.Fatalf("failed to initialize VolumePluginMgr: %v", err) } diff --git a/pkg/kubelet/token/BUILD b/pkg/kubelet/token/BUILD new file mode 100644 index 00000000000..d861681f574 --- /dev/null +++ b/pkg/kubelet/token/BUILD @@ -0,0 +1,40 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) + +go_library( + name = "go_default_library", + srcs = ["token_manager.go"], + importpath = "k8s.io/kubernetes/pkg/kubelet/token", + visibility = ["//visibility:public"], + deps = [ + "//vendor/github.com/golang/glog:go_default_library", + "//vendor/k8s.io/api/authentication/v1:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/util/clock:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library", + "//vendor/k8s.io/client-go/kubernetes/typed/core/v1:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["token_manager_test.go"], + embed = [":go_default_library"], + deps = [ + "//vendor/k8s.io/api/authentication/v1:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/util/clock:go_default_library", + ], +) diff --git a/pkg/kubelet/token/OWNERS b/pkg/kubelet/token/OWNERS new file mode 100644 index 00000000000..33904f77888 --- /dev/null +++ b/pkg/kubelet/token/OWNERS @@ -0,0 +1,6 @@ +approvers: +- mikedanese +reviewers: +- mikedanese +- awly +- tallclair diff --git a/pkg/kubelet/token/token_manager.go b/pkg/kubelet/token/token_manager.go new file mode 100644 index 00000000000..6d7ce23aea0 --- /dev/null +++ b/pkg/kubelet/token/token_manager.go @@ -0,0 +1,147 @@ +/* +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 token implements a manager of serviceaccount tokens for pods running +// on the node. +package token + +import ( + "fmt" + "sync" + "time" + + "github.com/golang/glog" + authenticationv1 "k8s.io/api/authentication/v1" + "k8s.io/apimachinery/pkg/util/clock" + "k8s.io/apimachinery/pkg/util/wait" + corev1 "k8s.io/client-go/kubernetes/typed/core/v1" +) + +const ( + maxTTL = 24 * time.Hour + gcPeriod = time.Minute +) + +// NewManager returns a new token manager. +func NewManager(c corev1.CoreV1Interface) *Manager { + m := &Manager{ + getToken: func(name, namespace string, tr *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) { + return c.ServiceAccounts(namespace).CreateToken(name, tr) + }, + cache: make(map[string]*authenticationv1.TokenRequest), + clock: clock.RealClock{}, + } + go wait.Forever(m.cleanup, gcPeriod) + return m +} + +// Manager manages service account tokens for pods. +type Manager struct { + + // cacheMutex guards the cache + cacheMutex sync.RWMutex + cache map[string]*authenticationv1.TokenRequest + + // mocked for testing + getToken func(name, namespace string, tr *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) + clock clock.Clock +} + +// GetServiceAccountToken gets a service account token for a pod from cache or +// from the TokenRequest API. This process is as follows: +// * Check the cache for the current token request. +// * If the token exists and does not require a refresh, return the current token. +// * Attempt to refresh the token. +// * If the token is refreshed successfully, save it in the cache and return the token. +// * If refresh fails and the old token is still valid, log an error and return the old token. +// * If refresh fails and the old token is no longer valid, return an error +func (m *Manager) GetServiceAccountToken(name, namespace string, tr *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) { + key := keyFunc(name, namespace, tr) + ctr, ok := m.get(key) + + if ok && !m.requiresRefresh(ctr) { + return ctr, nil + } + + tr, err := m.getToken(name, namespace, tr) + if err != nil { + switch { + case !ok: + return nil, fmt.Errorf("failed to fetch token: %v", err) + case m.expired(ctr): + return nil, fmt.Errorf("token %s expired and refresh failed: %v", key, err) + default: + glog.Errorf("couldn't update token %s: %v", key, err) + return ctr, nil + } + } + + m.set(key, tr) + return tr, nil +} + +func (m *Manager) cleanup() { + m.cacheMutex.Lock() + defer m.cacheMutex.Unlock() + for k, tr := range m.cache { + if m.expired(tr) { + delete(m.cache, k) + } + } +} + +func (m *Manager) get(key string) (*authenticationv1.TokenRequest, bool) { + m.cacheMutex.RLock() + defer m.cacheMutex.RUnlock() + ctr, ok := m.cache[key] + return ctr, ok +} + +func (m *Manager) set(key string, tr *authenticationv1.TokenRequest) { + m.cacheMutex.Lock() + defer m.cacheMutex.Unlock() + m.cache[key] = tr +} + +func (m *Manager) expired(t *authenticationv1.TokenRequest) bool { + return m.clock.Now().After(t.Status.ExpirationTimestamp.Time) +} + +// requiresRefresh returns true if the token is older than 80% of its total +// ttl, or if the token is older than 24 hours. +func (m *Manager) requiresRefresh(tr *authenticationv1.TokenRequest) bool { + if tr.Spec.ExpirationSeconds == nil { + glog.Errorf("expiration seconds was nil for tr: %#v", tr) + return false + } + now := m.clock.Now() + exp := tr.Status.ExpirationTimestamp.Time + iat := exp.Add(-1 * time.Duration(*tr.Spec.ExpirationSeconds) * time.Second) + + if now.After(iat.Add(maxTTL)) { + return true + } + // Require a refresh if within 20% of the TTL from the expiration time. + if now.After(exp.Add(-1 * time.Duration((*tr.Spec.ExpirationSeconds*20)/100) * time.Second)) { + return true + } + return false +} + +// keys should be nonconfidential and safe to log +func keyFunc(name, namespace string, tr *authenticationv1.TokenRequest) string { + return fmt.Sprintf("%q/%q/%#v", name, namespace, tr.Spec) +} diff --git a/pkg/kubelet/token/token_manager_test.go b/pkg/kubelet/token/token_manager_test.go new file mode 100644 index 00000000000..2d877652dd6 --- /dev/null +++ b/pkg/kubelet/token/token_manager_test.go @@ -0,0 +1,223 @@ +/* +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 token + +import ( + "fmt" + "testing" + "time" + + authenticationv1 "k8s.io/api/authentication/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/clock" +) + +func TestTokenCachingAndExpiration(t *testing.T) { + type suite struct { + clock *clock.FakeClock + tg *fakeTokenGetter + mgr *Manager + } + + cases := []struct { + name string + exp time.Duration + f func(t *testing.T, s *suite) + }{ + { + name: "rotate hour token expires in the last 12 minutes", + exp: time.Hour, + f: func(t *testing.T, s *suite) { + s.clock.SetTime(s.clock.Now().Add(50 * time.Minute)) + if _, err := s.mgr.GetServiceAccountToken("a", "b", &authenticationv1.TokenRequest{}); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if s.tg.count != 2 { + t.Fatalf("expected token to be refreshed: call count was %d", s.tg.count) + } + }, + }, + { + name: "rotate 24 hour token that expires in 40 hours", + exp: 40 * time.Hour, + f: func(t *testing.T, s *suite) { + s.clock.SetTime(s.clock.Now().Add(25 * time.Hour)) + if _, err := s.mgr.GetServiceAccountToken("a", "b", &authenticationv1.TokenRequest{}); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if s.tg.count != 2 { + t.Fatalf("expected token to be refreshed: call count was %d", s.tg.count) + } + }, + }, + { + name: "rotate hour token fails, old token is still valid, doesn't error", + exp: time.Hour, + f: func(t *testing.T, s *suite) { + s.clock.SetTime(s.clock.Now().Add(50 * time.Minute)) + tg := &fakeTokenGetter{ + err: fmt.Errorf("err"), + } + s.mgr.getToken = tg.getToken + tr, err := s.mgr.GetServiceAccountToken("a", "b", &authenticationv1.TokenRequest{}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if tr.Status.Token != "foo" { + t.Fatalf("unexpected token: %v", tr.Status.Token) + } + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + clock := clock.NewFakeClock(time.Time{}.Add(30 * 24 * time.Hour)) + expSecs := int64(c.exp.Seconds()) + s := &suite{ + clock: clock, + mgr: NewManager(nil), + tg: &fakeTokenGetter{ + tr: &authenticationv1.TokenRequest{ + Spec: authenticationv1.TokenRequestSpec{ + ExpirationSeconds: &expSecs, + }, + Status: authenticationv1.TokenRequestStatus{ + Token: "foo", + ExpirationTimestamp: metav1.Time{Time: clock.Now().Add(c.exp)}, + }, + }, + }, + } + s.mgr.getToken = s.tg.getToken + s.mgr.clock = s.clock + if _, err := s.mgr.GetServiceAccountToken("a", "b", &authenticationv1.TokenRequest{}); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if s.tg.count != 1 { + t.Fatalf("unexpected client call, got: %d, want: 1", s.tg.count) + } + + if _, err := s.mgr.GetServiceAccountToken("a", "b", &authenticationv1.TokenRequest{}); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if s.tg.count != 1 { + t.Fatalf("expected token to be served from cache: saw %d", s.tg.count) + } + + c.f(t, s) + }) + } +} + +func TestRequiresRefresh(t *testing.T) { + start := time.Now() + cases := []struct { + now, exp time.Time + expectRefresh bool + }{ + { + now: start.Add(10 * time.Minute), + exp: start.Add(60 * time.Minute), + expectRefresh: false, + }, + { + now: start.Add(50 * time.Minute), + exp: start.Add(60 * time.Minute), + expectRefresh: true, + }, + { + now: start.Add(25 * time.Hour), + exp: start.Add(60 * time.Hour), + expectRefresh: true, + }, + { + now: start.Add(70 * time.Minute), + exp: start.Add(60 * time.Minute), + expectRefresh: true, + }, + } + + for i, c := range cases { + t.Run(fmt.Sprint(i), func(t *testing.T) { + clock := clock.NewFakeClock(c.now) + secs := int64(c.exp.Sub(start).Seconds()) + tr := &authenticationv1.TokenRequest{ + Spec: authenticationv1.TokenRequestSpec{ + ExpirationSeconds: &secs, + }, + Status: authenticationv1.TokenRequestStatus{ + ExpirationTimestamp: metav1.Time{Time: c.exp}, + }, + } + mgr := NewManager(nil) + mgr.clock = clock + + rr := mgr.requiresRefresh(tr) + if rr != c.expectRefresh { + t.Fatalf("unexpected requiresRefresh result, got: %v, want: %v", rr, c.expectRefresh) + } + }) + } +} + +type fakeTokenGetter struct { + count int + tr *authenticationv1.TokenRequest + err error +} + +func (ftg *fakeTokenGetter) getToken(name, namespace string, tr *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) { + ftg.count++ + return ftg.tr, ftg.err +} + +func TestCleanup(t *testing.T) { + cases := []struct { + name string + relativeExp time.Duration + expectedCacheSize int + }{ + { + name: "don't cleanup unexpired tokens", + relativeExp: -1 * time.Hour, + expectedCacheSize: 0, + }, + { + name: "cleanup expired tokens", + relativeExp: time.Hour, + expectedCacheSize: 1, + }, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + clock := clock.NewFakeClock(time.Time{}.Add(24 * time.Hour)) + mgr := NewManager(nil) + mgr.clock = clock + + mgr.set("key", &authenticationv1.TokenRequest{ + Status: authenticationv1.TokenRequestStatus{ + ExpirationTimestamp: metav1.Time{Time: mgr.clock.Now().Add(c.relativeExp)}, + }, + }) + mgr.cleanup() + if got, want := len(mgr.cache), c.expectedCacheSize; got != want { + t.Fatalf("unexpected number of cache entries after cleanup, got: %d, want: %d", got, want) + } + }) + } +} diff --git a/pkg/kubelet/volume_host.go b/pkg/kubelet/volume_host.go index 9336d7cde3b..1d8bfcc9f68 100644 --- a/pkg/kubelet/volume_host.go +++ b/pkg/kubelet/volume_host.go @@ -23,6 +23,7 @@ import ( "github.com/golang/glog" + authenticationv1 "k8s.io/api/authentication/v1" "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" utilfeature "k8s.io/apiserver/pkg/util/feature" @@ -34,6 +35,7 @@ import ( "k8s.io/kubernetes/pkg/kubelet/container" "k8s.io/kubernetes/pkg/kubelet/mountpod" "k8s.io/kubernetes/pkg/kubelet/secret" + "k8s.io/kubernetes/pkg/kubelet/token" "k8s.io/kubernetes/pkg/util/io" "k8s.io/kubernetes/pkg/util/mount" "k8s.io/kubernetes/pkg/volume" @@ -50,6 +52,7 @@ func NewInitializedVolumePluginMgr( kubelet *Kubelet, secretManager secret.Manager, configMapManager configmap.Manager, + tokenManager *token.Manager, plugins []volume.VolumePlugin, prober volume.DynamicPluginProber) (*volume.VolumePluginMgr, error) { @@ -62,6 +65,7 @@ func NewInitializedVolumePluginMgr( volumePluginMgr: volume.VolumePluginMgr{}, secretManager: secretManager, configMapManager: configMapManager, + tokenManager: tokenManager, mountPodManager: mountPodManager, } @@ -85,6 +89,7 @@ type kubeletVolumeHost struct { kubelet *Kubelet volumePluginMgr volume.VolumePluginMgr secretManager secret.Manager + tokenManager *token.Manager configMapManager configmap.Manager mountPodManager mountpod.Manager } @@ -191,6 +196,10 @@ func (kvh *kubeletVolumeHost) GetConfigMapFunc() func(namespace, name string) (* return kvh.configMapManager.GetConfigMap } +func (kvh *kubeletVolumeHost) GetServiceAccountTokenFunc() func(namespace, name string, tr *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) { + return kvh.tokenManager.GetServiceAccountToken +} + func (kvh *kubeletVolumeHost) GetNodeLabels() (map[string]string, error) { node, err := kvh.kubelet.GetNode() if err != nil { diff --git a/pkg/volume/BUILD b/pkg/volume/BUILD index 097a994f8a1..28c8c8a68d8 100644 --- a/pkg/volume/BUILD +++ b/pkg/volume/BUILD @@ -56,6 +56,7 @@ go_library( "//pkg/volume/util/fs:go_default_library", "//pkg/volume/util/recyclerclient:go_default_library", "//vendor/github.com/golang/glog:go_default_library", + "//vendor/k8s.io/api/authentication/v1:go_default_library", "//vendor/k8s.io/api/core/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/resource:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", diff --git a/pkg/volume/plugins.go b/pkg/volume/plugins.go index b95ad471ca9..fbe0bd4c6d0 100644 --- a/pkg/volume/plugins.go +++ b/pkg/volume/plugins.go @@ -23,6 +23,7 @@ import ( "sync" "github.com/golang/glog" + authenticationv1 "k8s.io/api/authentication/v1" "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -336,6 +337,8 @@ type VolumeHost interface { // Returns a function that returns a configmap. GetConfigMapFunc() func(namespace, name string) (*v1.ConfigMap, error) + GetServiceAccountTokenFunc() func(namespace, name string, tr *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) + // Returns an interface that should be used to execute any utilities in volume plugins GetExec(pluginName string) mount.Exec diff --git a/pkg/volume/projected/BUILD b/pkg/volume/projected/BUILD index a8d0ce63461..2f3b388d431 100644 --- a/pkg/volume/projected/BUILD +++ b/pkg/volume/projected/BUILD @@ -28,6 +28,7 @@ go_library( srcs = ["projected.go"], importpath = "k8s.io/kubernetes/pkg/volume/projected", deps = [ + "//pkg/features:go_default_library", "//pkg/util/strings:go_default_library", "//pkg/volume:go_default_library", "//pkg/volume/configmap:go_default_library", @@ -35,11 +36,13 @@ go_library( "//pkg/volume/secret:go_default_library", "//pkg/volume/util:go_default_library", "//vendor/github.com/golang/glog:go_default_library", + "//vendor/k8s.io/api/authentication/v1:go_default_library", "//vendor/k8s.io/api/core/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/types:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/errors:go_default_library", + "//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library", ], ) diff --git a/pkg/volume/projected/projected.go b/pkg/volume/projected/projected.go index e75f10af5f3..2b55308d472 100644 --- a/pkg/volume/projected/projected.go +++ b/pkg/volume/projected/projected.go @@ -21,18 +21,22 @@ import ( "sort" "strings" - "github.com/golang/glog" + authenticationv1 "k8s.io/api/authentication/v1" "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" utilerrors "k8s.io/apimachinery/pkg/util/errors" + utilfeature "k8s.io/apiserver/pkg/util/feature" + "k8s.io/kubernetes/pkg/features" utilstrings "k8s.io/kubernetes/pkg/util/strings" "k8s.io/kubernetes/pkg/volume" "k8s.io/kubernetes/pkg/volume/configmap" "k8s.io/kubernetes/pkg/volume/downwardapi" "k8s.io/kubernetes/pkg/volume/secret" volumeutil "k8s.io/kubernetes/pkg/volume/util" + + "github.com/golang/glog" ) // ProbeVolumePlugins is the entry point for plugin detection in a package. @@ -45,9 +49,10 @@ const ( ) type projectedPlugin struct { - host volume.VolumeHost - getSecret func(namespace, name string) (*v1.Secret, error) - getConfigMap func(namespace, name string) (*v1.ConfigMap, error) + host volume.VolumeHost + getSecret func(namespace, name string) (*v1.Secret, error) + getConfigMap func(namespace, name string) (*v1.ConfigMap, error) + getServiceAccountToken func(namespace, name string, tr *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) } var _ volume.VolumePlugin = &projectedPlugin{} @@ -70,6 +75,7 @@ func (plugin *projectedPlugin) Init(host volume.VolumeHost) error { plugin.host = host plugin.getSecret = host.GetSecretFunc() plugin.getConfigMap = host.GetConfigMapFunc() + plugin.getServiceAccountToken = host.GetServiceAccountTokenFunc() return nil } @@ -236,7 +242,8 @@ func (s *projectedVolumeMounter) collectData() (map[string]volumeutil.FileProjec errlist := []error{} payload := make(map[string]volumeutil.FileProjection) for _, source := range s.source.Sources { - if source.Secret != nil { + switch { + case source.Secret != nil: optional := source.Secret.Optional != nil && *source.Secret.Optional secretapi, err := s.plugin.getSecret(s.pod.Namespace, source.Secret.Name) if err != nil { @@ -261,7 +268,7 @@ func (s *projectedVolumeMounter) collectData() (map[string]volumeutil.FileProjec for k, v := range secretPayload { payload[k] = v } - } else if source.ConfigMap != nil { + case source.ConfigMap != nil: optional := source.ConfigMap.Optional != nil && *source.ConfigMap.Optional configMap, err := s.plugin.getConfigMap(s.pod.Namespace, source.ConfigMap.Name) if err != nil { @@ -286,7 +293,7 @@ func (s *projectedVolumeMounter) collectData() (map[string]volumeutil.FileProjec for k, v := range configMapPayload { payload[k] = v } - } else if source.DownwardAPI != nil { + case source.DownwardAPI != nil: downwardAPIPayload, err := downwardapi.CollectData(source.DownwardAPI.Items, s.pod, s.plugin.host, s.source.DefaultMode) if err != nil { errlist = append(errlist, err) @@ -295,6 +302,34 @@ func (s *projectedVolumeMounter) collectData() (map[string]volumeutil.FileProjec for k, v := range downwardAPIPayload { payload[k] = v } + case source.ServiceAccountToken != nil: + if !utilfeature.DefaultFeatureGate.Enabled(features.TokenRequestProjection) { + errlist = append(errlist, fmt.Errorf("pod request ServiceAccountToken projection but the TokenRequestProjection feature was not enabled")) + continue + } + tp := source.ServiceAccountToken + tr, err := s.plugin.getServiceAccountToken(s.pod.Namespace, s.pod.Spec.ServiceAccountName, &authenticationv1.TokenRequest{ + Spec: authenticationv1.TokenRequestSpec{ + Audiences: []string{ + tp.Audience, + }, + ExpirationSeconds: tp.ExpirationSeconds, + BoundObjectRef: &authenticationv1.BoundObjectReference{ + APIVersion: "v1", + Kind: "Pod", + Name: s.pod.Name, + UID: s.pod.UID, + }, + }, + }) + if err != nil { + errlist = append(errlist, err) + continue + } + payload[tp.Path] = volumeutil.FileProjection{ + Data: []byte(tr.Status.Token), + Mode: 0600, + } } } return payload, utilerrors.NewAggregate(errlist) diff --git a/pkg/volume/testing/BUILD b/pkg/volume/testing/BUILD index edd95307657..6cbd8230c75 100644 --- a/pkg/volume/testing/BUILD +++ b/pkg/volume/testing/BUILD @@ -22,6 +22,7 @@ go_library( "//pkg/volume/util/recyclerclient:go_default_library", "//pkg/volume/util/volumepathhandler:go_default_library", "//vendor/github.com/stretchr/testify/mock:go_default_library", + "//vendor/k8s.io/api/authentication/v1:go_default_library", "//vendor/k8s.io/api/core/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/resource:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", diff --git a/pkg/volume/testing/testing.go b/pkg/volume/testing/testing.go index fb805812d6e..436b91de16e 100644 --- a/pkg/volume/testing/testing.go +++ b/pkg/volume/testing/testing.go @@ -27,6 +27,7 @@ import ( "testing" "time" + authenticationv1 "k8s.io/api/authentication/v1" "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -183,6 +184,12 @@ func (f *fakeVolumeHost) GetConfigMapFunc() func(namespace, name string) (*v1.Co } } +func (f *fakeVolumeHost) GetServiceAccountTokenFunc() func(string, string, *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) { + return func(namespace, name string, tr *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) { + return f.kubeClient.CoreV1().ServiceAccounts(namespace).CreateToken(name, tr) + } +} + func (f *fakeVolumeHost) GetNodeLabels() (map[string]string, error) { if f.nodeLabels == nil { f.nodeLabels = map[string]string{"test-label": "test-value"} diff --git a/plugin/pkg/admission/serviceaccount/admission.go b/plugin/pkg/admission/serviceaccount/admission.go index 6e245ecf8b0..7a831a295b6 100644 --- a/plugin/pkg/admission/serviceaccount/admission.go +++ b/plugin/pkg/admission/serviceaccount/admission.go @@ -208,6 +208,15 @@ func (s *serviceAccount) Validate(a admission.Attributes) (err error) { if hasSecrets { return admission.NewForbidden(a, fmt.Errorf("a mirror pod may not reference secrets")) } + for _, v := range pod.Spec.Volumes { + if proj := v.Projected; proj != nil { + for _, projSource := range proj.Sources { + if projSource.ServiceAccountToken != nil { + return admission.NewForbidden(a, fmt.Errorf("a mirror pod may not use ServiceAccountToken volume projections")) + } + } + } + } return nil } diff --git a/plugin/pkg/admission/serviceaccount/admission_test.go b/plugin/pkg/admission/serviceaccount/admission_test.go index d5167718647..18ed0ee9ee0 100644 --- a/plugin/pkg/admission/serviceaccount/admission_test.go +++ b/plugin/pkg/admission/serviceaccount/admission_test.go @@ -138,6 +138,31 @@ func TestRejectsMirrorPodWithSecretVolumes(t *testing.T) { } } +func TestRejectsMirrorPodWithServiceAccountTokenVolumeProjections(t *testing.T) { + pod := &api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + kubelet.ConfigMirrorAnnotationKey: "true", + }, + }, + Spec: api.PodSpec{ + Volumes: []api.Volume{ + {VolumeSource: api.VolumeSource{ + Projected: &api.ProjectedVolumeSource{ + Sources: []api.VolumeProjection{{ServiceAccountToken: &api.ServiceAccountTokenProjection{}}}, + }, + }, + }, + }, + }, + } + attrs := admission.NewAttributesRecord(pod, nil, api.Kind("Pod").WithVersion("version"), "myns", "myname", api.Resource("pods").WithVersion("version"), "", admission.Create, nil) + err := NewServiceAccount().Admit(attrs) + if err == nil { + t.Errorf("Expected a mirror pod to be prevented from referencing a ServiceAccountToken volume projection") + } +} + func TestAssignsDefaultServiceAccountAndToleratesMissingAPIToken(t *testing.T) { ns := "myns"