implement service account token projection

pull/8/head
Mike Danese 2018-05-14 11:21:05 -07:00
parent 46d2b47156
commit 91feb345aa
23 changed files with 553 additions and 12 deletions

View File

@ -25,6 +25,7 @@ go_library(
"//pkg/volume/util/operationexecutor:go_default_library", "//pkg/volume/util/operationexecutor:go_default_library",
"//pkg/volume/util/volumepathhandler:go_default_library", "//pkg/volume/util/volumepathhandler:go_default_library",
"//vendor/github.com/golang/glog: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/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/labels:go_default_library", "//vendor/k8s.io/apimachinery/pkg/labels:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library", "//vendor/k8s.io/apimachinery/pkg/types:go_default_library",

View File

@ -24,6 +24,7 @@ import (
"time" "time"
"github.com/golang/glog" "github.com/golang/glog"
authenticationv1 "k8s.io/api/authentication/v1"
"k8s.io/api/core/v1" "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/types" "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 { func (adc *attachDetachController) GetExec(pluginName string) mount.Exec {
return mount.NewOsExec() return mount.NewOsExec()
} }

View File

@ -25,6 +25,7 @@ go_library(
"//pkg/volume/util/operationexecutor:go_default_library", "//pkg/volume/util/operationexecutor:go_default_library",
"//pkg/volume/util/volumepathhandler:go_default_library", "//pkg/volume/util/volumepathhandler:go_default_library",
"//vendor/github.com/golang/glog: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/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/labels:go_default_library", "//vendor/k8s.io/apimachinery/pkg/labels:go_default_library",

View File

@ -26,6 +26,7 @@ import (
"github.com/golang/glog" "github.com/golang/glog"
authenticationv1 "k8s.io/api/authentication/v1"
"k8s.io/api/core/v1" "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/runtime" "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) { func (expc *expandController) GetNodeLabels() (map[string]string, error) {
return nil, fmt.Errorf("GetNodeLabels unsupported in expandController") return nil, fmt.Errorf("GetNodeLabels unsupported in expandController")
} }

View File

@ -34,6 +34,7 @@ go_library(
"//pkg/volume/util:go_default_library", "//pkg/volume/util:go_default_library",
"//pkg/volume/util/recyclerclient:go_default_library", "//pkg/volume/util/recyclerclient:go_default_library",
"//vendor/github.com/golang/glog: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/core/v1:go_default_library",
"//vendor/k8s.io/api/storage/v1:go_default_library", "//vendor/k8s.io/api/storage/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",

View File

@ -20,6 +20,7 @@ import (
"fmt" "fmt"
"net" "net"
authenticationv1 "k8s.io/api/authentication/v1"
"k8s.io/api/core/v1" "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
clientset "k8s.io/client-go/kubernetes" clientset "k8s.io/client-go/kubernetes"
@ -94,18 +95,24 @@ func (ctrl *PersistentVolumeController) GetNodeAllocatable() (v1.ResourceList, e
return v1.ResourceList{}, nil 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 func(_, _ string) (*v1.Secret, error) {
return nil, fmt.Errorf("GetSecret unsupported in PersistentVolumeController") 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 func(_, _ string) (*v1.ConfigMap, error) {
return nil, fmt.Errorf("GetConfigMap unsupported in PersistentVolumeController") 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 { func (adc *PersistentVolumeController) GetExec(pluginName string) mount.Exec {
return mount.NewOsExec() return mount.NewOsExec()
} }

View File

@ -80,6 +80,7 @@ go_library(
"//pkg/kubelet/stats:go_default_library", "//pkg/kubelet/stats:go_default_library",
"//pkg/kubelet/status:go_default_library", "//pkg/kubelet/status:go_default_library",
"//pkg/kubelet/sysctl:go_default_library", "//pkg/kubelet/sysctl:go_default_library",
"//pkg/kubelet/token:go_default_library",
"//pkg/kubelet/types:go_default_library", "//pkg/kubelet/types:go_default_library",
"//pkg/kubelet/util:go_default_library", "//pkg/kubelet/util:go_default_library",
"//pkg/kubelet/util/format:go_default_library", "//pkg/kubelet/util/format:go_default_library",
@ -112,6 +113,7 @@ go_library(
"//vendor/github.com/google/cadvisor/events:go_default_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/v1:go_default_library",
"//vendor/github.com/google/cadvisor/info/v2: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/api/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/resource:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/resource:go_default_library",
@ -190,6 +192,7 @@ go_test(
"//pkg/kubelet/stats:go_default_library", "//pkg/kubelet/stats:go_default_library",
"//pkg/kubelet/status:go_default_library", "//pkg/kubelet/status:go_default_library",
"//pkg/kubelet/status/testing:go_default_library", "//pkg/kubelet/status/testing:go_default_library",
"//pkg/kubelet/token:go_default_library",
"//pkg/kubelet/types:go_default_library", "//pkg/kubelet/types:go_default_library",
"//pkg/kubelet/util/queue:go_default_library", "//pkg/kubelet/util/queue:go_default_library",
"//pkg/kubelet/util/sliceutils:go_default_library", "//pkg/kubelet/util/sliceutils:go_default_library",
@ -281,6 +284,7 @@ filegroup(
"//pkg/kubelet/stats:all-srcs", "//pkg/kubelet/stats:all-srcs",
"//pkg/kubelet/status:all-srcs", "//pkg/kubelet/status:all-srcs",
"//pkg/kubelet/sysctl:all-srcs", "//pkg/kubelet/sysctl:all-srcs",
"//pkg/kubelet/token:all-srcs",
"//pkg/kubelet/types:all-srcs", "//pkg/kubelet/types:all-srcs",
"//pkg/kubelet/util:all-srcs", "//pkg/kubelet/util:all-srcs",
"//pkg/kubelet/volumemanager:all-srcs", "//pkg/kubelet/volumemanager:all-srcs",

View File

@ -90,6 +90,7 @@ import (
"k8s.io/kubernetes/pkg/kubelet/stats" "k8s.io/kubernetes/pkg/kubelet/stats"
"k8s.io/kubernetes/pkg/kubelet/status" "k8s.io/kubernetes/pkg/kubelet/status"
"k8s.io/kubernetes/pkg/kubelet/sysctl" "k8s.io/kubernetes/pkg/kubelet/sysctl"
"k8s.io/kubernetes/pkg/kubelet/token"
kubetypes "k8s.io/kubernetes/pkg/kubelet/types" kubetypes "k8s.io/kubernetes/pkg/kubelet/types"
"k8s.io/kubernetes/pkg/kubelet/util/format" "k8s.io/kubernetes/pkg/kubelet/util/format"
"k8s.io/kubernetes/pkg/kubelet/util/manager" "k8s.io/kubernetes/pkg/kubelet/util/manager"
@ -778,8 +779,10 @@ func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration,
containerRefManager, containerRefManager,
kubeDeps.Recorder) kubeDeps.Recorder)
tokenManager := token.NewManager(kubeDeps.KubeClient.CoreV1())
klet.volumePluginMgr, err = klet.volumePluginMgr, err =
NewInitializedVolumePluginMgr(klet, secretManager, configMapManager, kubeDeps.VolumePlugins, kubeDeps.DynamicPluginProber) NewInitializedVolumePluginMgr(klet, secretManager, configMapManager, tokenManager, kubeDeps.VolumePlugins, kubeDeps.DynamicPluginProber)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -61,6 +61,7 @@ import (
"k8s.io/kubernetes/pkg/kubelet/stats" "k8s.io/kubernetes/pkg/kubelet/stats"
"k8s.io/kubernetes/pkg/kubelet/status" "k8s.io/kubernetes/pkg/kubelet/status"
statustest "k8s.io/kubernetes/pkg/kubelet/status/testing" statustest "k8s.io/kubernetes/pkg/kubelet/status/testing"
"k8s.io/kubernetes/pkg/kubelet/token"
kubetypes "k8s.io/kubernetes/pkg/kubelet/types" kubetypes "k8s.io/kubernetes/pkg/kubelet/types"
"k8s.io/kubernetes/pkg/kubelet/util/queue" "k8s.io/kubernetes/pkg/kubelet/util/queue"
kubeletvolume "k8s.io/kubernetes/pkg/kubelet/volumemanager" kubeletvolume "k8s.io/kubernetes/pkg/kubelet/volumemanager"
@ -325,7 +326,7 @@ func newTestKubeletWithImageList(
var prober volume.DynamicPluginProber = nil // TODO (#51147) inject mock var prober volume.DynamicPluginProber = nil // TODO (#51147) inject mock
kubelet.volumePluginMgr, err = 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") require.NoError(t, err, "Failed to initialize VolumePluginMgr")
kubelet.mounter = &mount.FakeMounter{} kubelet.mounter = &mount.FakeMounter{}

View File

@ -90,7 +90,7 @@ func TestRunOnce(t *testing.T) {
plug := &volumetest.FakeVolumePlugin{PluginName: "fake", Host: nil} plug := &volumetest.FakeVolumePlugin{PluginName: "fake", Host: nil}
kb.volumePluginMgr, err = 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 { if err != nil {
t.Fatalf("failed to initialize VolumePluginMgr: %v", err) t.Fatalf("failed to initialize VolumePluginMgr: %v", err)
} }

40
pkg/kubelet/token/BUILD Normal file
View File

@ -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",
],
)

6
pkg/kubelet/token/OWNERS Normal file
View File

@ -0,0 +1,6 @@
approvers:
- mikedanese
reviewers:
- mikedanese
- awly
- tallclair

View File

@ -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)
}

View File

@ -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)
}
})
}
}

View File

@ -23,6 +23,7 @@ import (
"github.com/golang/glog" "github.com/golang/glog"
authenticationv1 "k8s.io/api/authentication/v1"
"k8s.io/api/core/v1" "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
utilfeature "k8s.io/apiserver/pkg/util/feature" utilfeature "k8s.io/apiserver/pkg/util/feature"
@ -34,6 +35,7 @@ import (
"k8s.io/kubernetes/pkg/kubelet/container" "k8s.io/kubernetes/pkg/kubelet/container"
"k8s.io/kubernetes/pkg/kubelet/mountpod" "k8s.io/kubernetes/pkg/kubelet/mountpod"
"k8s.io/kubernetes/pkg/kubelet/secret" "k8s.io/kubernetes/pkg/kubelet/secret"
"k8s.io/kubernetes/pkg/kubelet/token"
"k8s.io/kubernetes/pkg/util/io" "k8s.io/kubernetes/pkg/util/io"
"k8s.io/kubernetes/pkg/util/mount" "k8s.io/kubernetes/pkg/util/mount"
"k8s.io/kubernetes/pkg/volume" "k8s.io/kubernetes/pkg/volume"
@ -50,6 +52,7 @@ func NewInitializedVolumePluginMgr(
kubelet *Kubelet, kubelet *Kubelet,
secretManager secret.Manager, secretManager secret.Manager,
configMapManager configmap.Manager, configMapManager configmap.Manager,
tokenManager *token.Manager,
plugins []volume.VolumePlugin, plugins []volume.VolumePlugin,
prober volume.DynamicPluginProber) (*volume.VolumePluginMgr, error) { prober volume.DynamicPluginProber) (*volume.VolumePluginMgr, error) {
@ -62,6 +65,7 @@ func NewInitializedVolumePluginMgr(
volumePluginMgr: volume.VolumePluginMgr{}, volumePluginMgr: volume.VolumePluginMgr{},
secretManager: secretManager, secretManager: secretManager,
configMapManager: configMapManager, configMapManager: configMapManager,
tokenManager: tokenManager,
mountPodManager: mountPodManager, mountPodManager: mountPodManager,
} }
@ -85,6 +89,7 @@ type kubeletVolumeHost struct {
kubelet *Kubelet kubelet *Kubelet
volumePluginMgr volume.VolumePluginMgr volumePluginMgr volume.VolumePluginMgr
secretManager secret.Manager secretManager secret.Manager
tokenManager *token.Manager
configMapManager configmap.Manager configMapManager configmap.Manager
mountPodManager mountpod.Manager mountPodManager mountpod.Manager
} }
@ -191,6 +196,10 @@ func (kvh *kubeletVolumeHost) GetConfigMapFunc() func(namespace, name string) (*
return kvh.configMapManager.GetConfigMap 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) { func (kvh *kubeletVolumeHost) GetNodeLabels() (map[string]string, error) {
node, err := kvh.kubelet.GetNode() node, err := kvh.kubelet.GetNode()
if err != nil { if err != nil {

View File

@ -56,6 +56,7 @@ go_library(
"//pkg/volume/util/fs:go_default_library", "//pkg/volume/util/fs:go_default_library",
"//pkg/volume/util/recyclerclient:go_default_library", "//pkg/volume/util/recyclerclient:go_default_library",
"//vendor/github.com/golang/glog: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/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/resource:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/resource:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",

View File

@ -23,6 +23,7 @@ import (
"sync" "sync"
"github.com/golang/glog" "github.com/golang/glog"
authenticationv1 "k8s.io/api/authentication/v1"
"k8s.io/api/core/v1" "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -336,6 +337,8 @@ type VolumeHost interface {
// Returns a function that returns a configmap. // Returns a function that returns a configmap.
GetConfigMapFunc() func(namespace, name string) (*v1.ConfigMap, error) 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 // Returns an interface that should be used to execute any utilities in volume plugins
GetExec(pluginName string) mount.Exec GetExec(pluginName string) mount.Exec

View File

@ -28,6 +28,7 @@ go_library(
srcs = ["projected.go"], srcs = ["projected.go"],
importpath = "k8s.io/kubernetes/pkg/volume/projected", importpath = "k8s.io/kubernetes/pkg/volume/projected",
deps = [ deps = [
"//pkg/features:go_default_library",
"//pkg/util/strings:go_default_library", "//pkg/util/strings:go_default_library",
"//pkg/volume:go_default_library", "//pkg/volume:go_default_library",
"//pkg/volume/configmap:go_default_library", "//pkg/volume/configmap:go_default_library",
@ -35,11 +36,13 @@ go_library(
"//pkg/volume/secret:go_default_library", "//pkg/volume/secret:go_default_library",
"//pkg/volume/util:go_default_library", "//pkg/volume/util:go_default_library",
"//vendor/github.com/golang/glog: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/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors: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/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/types: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/apimachinery/pkg/util/errors:go_default_library",
"//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library",
], ],
) )

View File

@ -21,18 +21,22 @@ import (
"sort" "sort"
"strings" "strings"
"github.com/golang/glog" authenticationv1 "k8s.io/api/authentication/v1"
"k8s.io/api/core/v1" "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
utilerrors "k8s.io/apimachinery/pkg/util/errors" 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" utilstrings "k8s.io/kubernetes/pkg/util/strings"
"k8s.io/kubernetes/pkg/volume" "k8s.io/kubernetes/pkg/volume"
"k8s.io/kubernetes/pkg/volume/configmap" "k8s.io/kubernetes/pkg/volume/configmap"
"k8s.io/kubernetes/pkg/volume/downwardapi" "k8s.io/kubernetes/pkg/volume/downwardapi"
"k8s.io/kubernetes/pkg/volume/secret" "k8s.io/kubernetes/pkg/volume/secret"
volumeutil "k8s.io/kubernetes/pkg/volume/util" volumeutil "k8s.io/kubernetes/pkg/volume/util"
"github.com/golang/glog"
) )
// ProbeVolumePlugins is the entry point for plugin detection in a package. // ProbeVolumePlugins is the entry point for plugin detection in a package.
@ -48,6 +52,7 @@ type projectedPlugin struct {
host volume.VolumeHost host volume.VolumeHost
getSecret func(namespace, name string) (*v1.Secret, error) getSecret func(namespace, name string) (*v1.Secret, error)
getConfigMap func(namespace, name string) (*v1.ConfigMap, error) getConfigMap func(namespace, name string) (*v1.ConfigMap, error)
getServiceAccountToken func(namespace, name string, tr *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error)
} }
var _ volume.VolumePlugin = &projectedPlugin{} var _ volume.VolumePlugin = &projectedPlugin{}
@ -70,6 +75,7 @@ func (plugin *projectedPlugin) Init(host volume.VolumeHost) error {
plugin.host = host plugin.host = host
plugin.getSecret = host.GetSecretFunc() plugin.getSecret = host.GetSecretFunc()
plugin.getConfigMap = host.GetConfigMapFunc() plugin.getConfigMap = host.GetConfigMapFunc()
plugin.getServiceAccountToken = host.GetServiceAccountTokenFunc()
return nil return nil
} }
@ -236,7 +242,8 @@ func (s *projectedVolumeMounter) collectData() (map[string]volumeutil.FileProjec
errlist := []error{} errlist := []error{}
payload := make(map[string]volumeutil.FileProjection) payload := make(map[string]volumeutil.FileProjection)
for _, source := range s.source.Sources { for _, source := range s.source.Sources {
if source.Secret != nil { switch {
case source.Secret != nil:
optional := source.Secret.Optional != nil && *source.Secret.Optional optional := source.Secret.Optional != nil && *source.Secret.Optional
secretapi, err := s.plugin.getSecret(s.pod.Namespace, source.Secret.Name) secretapi, err := s.plugin.getSecret(s.pod.Namespace, source.Secret.Name)
if err != nil { if err != nil {
@ -261,7 +268,7 @@ func (s *projectedVolumeMounter) collectData() (map[string]volumeutil.FileProjec
for k, v := range secretPayload { for k, v := range secretPayload {
payload[k] = v payload[k] = v
} }
} else if source.ConfigMap != nil { case source.ConfigMap != nil:
optional := source.ConfigMap.Optional != nil && *source.ConfigMap.Optional optional := source.ConfigMap.Optional != nil && *source.ConfigMap.Optional
configMap, err := s.plugin.getConfigMap(s.pod.Namespace, source.ConfigMap.Name) configMap, err := s.plugin.getConfigMap(s.pod.Namespace, source.ConfigMap.Name)
if err != nil { if err != nil {
@ -286,7 +293,7 @@ func (s *projectedVolumeMounter) collectData() (map[string]volumeutil.FileProjec
for k, v := range configMapPayload { for k, v := range configMapPayload {
payload[k] = v 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) downwardAPIPayload, err := downwardapi.CollectData(source.DownwardAPI.Items, s.pod, s.plugin.host, s.source.DefaultMode)
if err != nil { if err != nil {
errlist = append(errlist, err) errlist = append(errlist, err)
@ -295,6 +302,34 @@ func (s *projectedVolumeMounter) collectData() (map[string]volumeutil.FileProjec
for k, v := range downwardAPIPayload { for k, v := range downwardAPIPayload {
payload[k] = v 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) return payload, utilerrors.NewAggregate(errlist)

View File

@ -22,6 +22,7 @@ go_library(
"//pkg/volume/util/recyclerclient:go_default_library", "//pkg/volume/util/recyclerclient:go_default_library",
"//pkg/volume/util/volumepathhandler:go_default_library", "//pkg/volume/util/volumepathhandler:go_default_library",
"//vendor/github.com/stretchr/testify/mock: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/api/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/resource:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/resource:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",

View File

@ -27,6 +27,7 @@ import (
"testing" "testing"
"time" "time"
authenticationv1 "k8s.io/api/authentication/v1"
"k8s.io/api/core/v1" "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 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) { func (f *fakeVolumeHost) GetNodeLabels() (map[string]string, error) {
if f.nodeLabels == nil { if f.nodeLabels == nil {
f.nodeLabels = map[string]string{"test-label": "test-value"} f.nodeLabels = map[string]string{"test-label": "test-value"}

View File

@ -208,6 +208,15 @@ func (s *serviceAccount) Validate(a admission.Attributes) (err error) {
if hasSecrets { if hasSecrets {
return admission.NewForbidden(a, fmt.Errorf("a mirror pod may not reference secrets")) 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 return nil
} }

View File

@ -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) { func TestAssignsDefaultServiceAccountAndToleratesMissingAPIToken(t *testing.T) {
ns := "myns" ns := "myns"