mirror of https://github.com/k3s-io/k3s
Delete bootstrap controller
parent
5d66fd8d98
commit
e661a8bc3b
|
@ -1,52 +0,0 @@
|
|||
/*
|
||||
Copyright 2016 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 app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"net/http"
|
||||
|
||||
"k8s.io/kubernetes/pkg/controller/bootstrap"
|
||||
)
|
||||
|
||||
func startBootstrapSignerController(ctx ControllerContext) (http.Handler, bool, error) {
|
||||
bsc, err := bootstrap.NewBootstrapSigner(
|
||||
ctx.ClientBuilder.ClientOrDie("bootstrap-signer"),
|
||||
ctx.InformerFactory.Core().V1().Secrets(),
|
||||
ctx.InformerFactory.Core().V1().ConfigMaps(),
|
||||
bootstrap.DefaultBootstrapSignerOptions(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, true, fmt.Errorf("error creating BootstrapSigner controller: %v", err)
|
||||
}
|
||||
go bsc.Run(ctx.Stop)
|
||||
return nil, true, nil
|
||||
}
|
||||
|
||||
func startTokenCleanerController(ctx ControllerContext) (http.Handler, bool, error) {
|
||||
tcc, err := bootstrap.NewTokenCleaner(
|
||||
ctx.ClientBuilder.ClientOrDie("token-cleaner"),
|
||||
ctx.InformerFactory.Core().V1().Secrets(),
|
||||
bootstrap.DefaultTokenCleanerOptions(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, true, fmt.Errorf("error creating TokenCleaner controller: %v", err)
|
||||
}
|
||||
go tcc.Run(ctx.Stop)
|
||||
return nil, true, nil
|
||||
}
|
|
@ -361,8 +361,6 @@ func NewControllerInitializers(loopMode ControllerLoopMode) map[string]InitFunc
|
|||
controllers["csrapproving"] = startCSRApprovingController
|
||||
controllers["csrcleaner"] = startCSRCleanerController
|
||||
controllers["ttl"] = startTTLController
|
||||
controllers["bootstrapsigner"] = startBootstrapSignerController
|
||||
controllers["tokencleaner"] = startTokenCleanerController
|
||||
controllers["nodeipam"] = startNodeIpamController
|
||||
controllers["nodelifecycle"] = startNodeLifecycleController
|
||||
if loopMode == IncludeCloudLoops {
|
||||
|
|
|
@ -1,78 +0,0 @@
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
load(
|
||||
"@io_bazel_rules_go//go:def.bzl",
|
||||
"go_library",
|
||||
"go_test",
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = [
|
||||
"bootstrapsigner_test.go",
|
||||
"common_test.go",
|
||||
"jws_test.go",
|
||||
"tokencleaner_test.go",
|
||||
"util_test.go",
|
||||
],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//pkg/apis/core:go_default_library",
|
||||
"//pkg/apis/core/helper:go_default_library",
|
||||
"//pkg/controller:go_default_library",
|
||||
"//staging/src/k8s.io/api/core/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/informers:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/informers/core/v1:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/kubernetes/fake:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/testing:go_default_library",
|
||||
"//staging/src/k8s.io/cluster-bootstrap/token/api:go_default_library",
|
||||
"//vendor/github.com/davecgh/go-spew/spew:go_default_library",
|
||||
"//vendor/github.com/stretchr/testify/assert:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = [
|
||||
"bootstrapsigner.go",
|
||||
"doc.go",
|
||||
"jws.go",
|
||||
"tokencleaner.go",
|
||||
"util.go",
|
||||
],
|
||||
importpath = "k8s.io/kubernetes/pkg/controller/bootstrap",
|
||||
deps = [
|
||||
"//pkg/apis/core:go_default_library",
|
||||
"//pkg/controller:go_default_library",
|
||||
"//pkg/util/metrics:go_default_library",
|
||||
"//staging/src/k8s.io/api/core/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/runtime:go_default_library",
|
||||
"//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/informers/core/v1:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/kubernetes:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/listers/core/v1:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/tools/cache:go_default_library",
|
||||
"//staging/src/k8s.io/client-go/util/workqueue:go_default_library",
|
||||
"//staging/src/k8s.io/cluster-bootstrap/token/api:go_default_library",
|
||||
"//vendor/gopkg.in/square/go-jose.v2:go_default_library",
|
||||
"//vendor/k8s.io/klog:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
)
|
|
@ -1,307 +0,0 @@
|
|||
/*
|
||||
Copyright 2016 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 bootstrap
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"k8s.io/klog"
|
||||
|
||||
"fmt"
|
||||
"k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
informers "k8s.io/client-go/informers/core/v1"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
corelisters "k8s.io/client-go/listers/core/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
bootstrapapi "k8s.io/cluster-bootstrap/token/api"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
"k8s.io/kubernetes/pkg/controller"
|
||||
"k8s.io/kubernetes/pkg/util/metrics"
|
||||
)
|
||||
|
||||
// BootstrapSignerOptions contains options for the BootstrapSigner
|
||||
type BootstrapSignerOptions struct {
|
||||
// ConfigMapNamespace is the namespace of the ConfigMap
|
||||
ConfigMapNamespace string
|
||||
|
||||
// ConfigMapName is the name for the ConfigMap
|
||||
ConfigMapName string
|
||||
|
||||
// TokenSecretNamespace string is the namespace for token Secrets.
|
||||
TokenSecretNamespace string
|
||||
|
||||
// ConfigMapResynce is the time.Duration at which to fully re-list configmaps.
|
||||
// If zero, re-list will be delayed as long as possible
|
||||
ConfigMapResync time.Duration
|
||||
|
||||
// SecretResync is the time.Duration at which to fully re-list secrets.
|
||||
// If zero, re-list will be delayed as long as possible
|
||||
SecretResync time.Duration
|
||||
}
|
||||
|
||||
// DefaultBootstrapSignerOptions returns a set of default options for creating a
|
||||
// BootstrapSigner
|
||||
func DefaultBootstrapSignerOptions() BootstrapSignerOptions {
|
||||
return BootstrapSignerOptions{
|
||||
ConfigMapNamespace: api.NamespacePublic,
|
||||
ConfigMapName: bootstrapapi.ConfigMapClusterInfo,
|
||||
TokenSecretNamespace: api.NamespaceSystem,
|
||||
}
|
||||
}
|
||||
|
||||
// BootstrapSigner is a controller that signs a ConfigMap with a set of tokens.
|
||||
type BootstrapSigner struct {
|
||||
client clientset.Interface
|
||||
configMapKey string
|
||||
configMapName string
|
||||
configMapNamespace string
|
||||
secretNamespace string
|
||||
|
||||
// syncQueue handles synchronizing updates to the ConfigMap. We'll only ever
|
||||
// have one item (Named <ConfigMapName>) in this queue. We are using it
|
||||
// serializes and collapses updates as they can come from both the ConfigMap
|
||||
// and Secrets controllers.
|
||||
syncQueue workqueue.RateLimitingInterface
|
||||
|
||||
secretLister corelisters.SecretLister
|
||||
secretSynced cache.InformerSynced
|
||||
|
||||
configMapLister corelisters.ConfigMapLister
|
||||
configMapSynced cache.InformerSynced
|
||||
}
|
||||
|
||||
// NewBootstrapSigner returns a new *BootstrapSigner.
|
||||
func NewBootstrapSigner(cl clientset.Interface, secrets informers.SecretInformer, configMaps informers.ConfigMapInformer, options BootstrapSignerOptions) (*BootstrapSigner, error) {
|
||||
e := &BootstrapSigner{
|
||||
client: cl,
|
||||
configMapKey: options.ConfigMapNamespace + "/" + options.ConfigMapName,
|
||||
configMapName: options.ConfigMapName,
|
||||
configMapNamespace: options.ConfigMapNamespace,
|
||||
secretNamespace: options.TokenSecretNamespace,
|
||||
secretLister: secrets.Lister(),
|
||||
secretSynced: secrets.Informer().HasSynced,
|
||||
configMapLister: configMaps.Lister(),
|
||||
configMapSynced: configMaps.Informer().HasSynced,
|
||||
syncQueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "bootstrap_signer_queue"),
|
||||
}
|
||||
if cl.CoreV1().RESTClient().GetRateLimiter() != nil {
|
||||
if err := metrics.RegisterMetricAndTrackRateLimiterUsage("bootstrap_signer", cl.CoreV1().RESTClient().GetRateLimiter()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
configMaps.Informer().AddEventHandlerWithResyncPeriod(
|
||||
cache.FilteringResourceEventHandler{
|
||||
FilterFunc: func(obj interface{}) bool {
|
||||
switch t := obj.(type) {
|
||||
case *v1.ConfigMap:
|
||||
return t.Name == options.ConfigMapName && t.Namespace == options.ConfigMapNamespace
|
||||
default:
|
||||
utilruntime.HandleError(fmt.Errorf("object passed to %T that is not expected: %T", e, obj))
|
||||
return false
|
||||
}
|
||||
},
|
||||
Handler: cache.ResourceEventHandlerFuncs{
|
||||
AddFunc: func(_ interface{}) { e.pokeConfigMapSync() },
|
||||
UpdateFunc: func(_, _ interface{}) { e.pokeConfigMapSync() },
|
||||
},
|
||||
},
|
||||
options.ConfigMapResync,
|
||||
)
|
||||
|
||||
secrets.Informer().AddEventHandlerWithResyncPeriod(
|
||||
cache.FilteringResourceEventHandler{
|
||||
FilterFunc: func(obj interface{}) bool {
|
||||
switch t := obj.(type) {
|
||||
case *v1.Secret:
|
||||
return t.Type == bootstrapapi.SecretTypeBootstrapToken && t.Namespace == e.secretNamespace
|
||||
default:
|
||||
utilruntime.HandleError(fmt.Errorf("object passed to %T that is not expected: %T", e, obj))
|
||||
return false
|
||||
}
|
||||
},
|
||||
Handler: cache.ResourceEventHandlerFuncs{
|
||||
AddFunc: func(_ interface{}) { e.pokeConfigMapSync() },
|
||||
UpdateFunc: func(_, _ interface{}) { e.pokeConfigMapSync() },
|
||||
DeleteFunc: func(_ interface{}) { e.pokeConfigMapSync() },
|
||||
},
|
||||
},
|
||||
options.SecretResync,
|
||||
)
|
||||
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// Run runs controller loops and returns when they are done
|
||||
func (e *BootstrapSigner) Run(stopCh <-chan struct{}) {
|
||||
// Shut down queues
|
||||
defer utilruntime.HandleCrash()
|
||||
defer e.syncQueue.ShutDown()
|
||||
|
||||
if !controller.WaitForCacheSync("bootstrap_signer", stopCh, e.configMapSynced, e.secretSynced) {
|
||||
return
|
||||
}
|
||||
|
||||
klog.V(5).Infof("Starting workers")
|
||||
go wait.Until(e.serviceConfigMapQueue, 0, stopCh)
|
||||
<-stopCh
|
||||
klog.V(1).Infof("Shutting down")
|
||||
}
|
||||
|
||||
func (e *BootstrapSigner) pokeConfigMapSync() {
|
||||
e.syncQueue.Add(e.configMapKey)
|
||||
}
|
||||
|
||||
func (e *BootstrapSigner) serviceConfigMapQueue() {
|
||||
key, quit := e.syncQueue.Get()
|
||||
if quit {
|
||||
return
|
||||
}
|
||||
defer e.syncQueue.Done(key)
|
||||
|
||||
e.signConfigMap()
|
||||
}
|
||||
|
||||
// signConfigMap computes the signatures on our latest cached objects and writes
|
||||
// back if necessary.
|
||||
func (e *BootstrapSigner) signConfigMap() {
|
||||
origCM := e.getConfigMap()
|
||||
|
||||
if origCM == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var needUpdate = false
|
||||
|
||||
newCM := origCM.DeepCopy()
|
||||
|
||||
// First capture the config we are signing
|
||||
content, ok := newCM.Data[bootstrapapi.KubeConfigKey]
|
||||
if !ok {
|
||||
klog.V(3).Infof("No %s key in %s/%s ConfigMap", bootstrapapi.KubeConfigKey, origCM.Namespace, origCM.Name)
|
||||
return
|
||||
}
|
||||
|
||||
// Next remove and save all existing signatures
|
||||
sigs := map[string]string{}
|
||||
for key, value := range newCM.Data {
|
||||
if strings.HasPrefix(key, bootstrapapi.JWSSignatureKeyPrefix) {
|
||||
tokenID := strings.TrimPrefix(key, bootstrapapi.JWSSignatureKeyPrefix)
|
||||
sigs[tokenID] = value
|
||||
delete(newCM.Data, key)
|
||||
}
|
||||
}
|
||||
|
||||
// Now recompute signatures and store them on the new map
|
||||
tokens := e.getTokens()
|
||||
for tokenID, tokenValue := range tokens {
|
||||
sig, err := computeDetachedSig(content, tokenID, tokenValue)
|
||||
if err != nil {
|
||||
utilruntime.HandleError(err)
|
||||
}
|
||||
|
||||
// Check to see if this signature is changed or new.
|
||||
oldSig, _ := sigs[tokenID]
|
||||
if sig != oldSig {
|
||||
needUpdate = true
|
||||
}
|
||||
delete(sigs, tokenID)
|
||||
|
||||
newCM.Data[bootstrapapi.JWSSignatureKeyPrefix+tokenID] = sig
|
||||
}
|
||||
|
||||
// If we have signatures left over we know that some signatures were
|
||||
// removed. We now need to update the ConfigMap
|
||||
if len(sigs) != 0 {
|
||||
needUpdate = true
|
||||
}
|
||||
|
||||
if needUpdate {
|
||||
e.updateConfigMap(newCM)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *BootstrapSigner) updateConfigMap(cm *v1.ConfigMap) {
|
||||
_, err := e.client.CoreV1().ConfigMaps(cm.Namespace).Update(cm)
|
||||
if err != nil && !apierrors.IsConflict(err) && !apierrors.IsNotFound(err) {
|
||||
klog.V(3).Infof("Error updating ConfigMap: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// getConfigMap gets the ConfigMap we are interested in
|
||||
func (e *BootstrapSigner) getConfigMap() *v1.ConfigMap {
|
||||
configMap, err := e.configMapLister.ConfigMaps(e.configMapNamespace).Get(e.configMapName)
|
||||
|
||||
// If we can't get the configmap just return nil. The resync will eventually
|
||||
// sync things up.
|
||||
if err != nil {
|
||||
if !apierrors.IsNotFound(err) {
|
||||
utilruntime.HandleError(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return configMap
|
||||
}
|
||||
|
||||
func (e *BootstrapSigner) listSecrets() []*v1.Secret {
|
||||
secrets, err := e.secretLister.Secrets(e.secretNamespace).List(labels.Everything())
|
||||
if err != nil {
|
||||
utilruntime.HandleError(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
items := []*v1.Secret{}
|
||||
for _, secret := range secrets {
|
||||
if secret.Type == bootstrapapi.SecretTypeBootstrapToken {
|
||||
items = append(items, secret)
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
// getTokens returns a map of tokenID->tokenSecret. It ensures the token is
|
||||
// valid for signing.
|
||||
func (e *BootstrapSigner) getTokens() map[string]string {
|
||||
ret := map[string]string{}
|
||||
secretObjs := e.listSecrets()
|
||||
for _, secret := range secretObjs {
|
||||
tokenID, tokenSecret, ok := validateSecretForSigning(secret)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check and warn for duplicate secrets. Behavior here will be undefined.
|
||||
if _, ok := ret[tokenID]; ok {
|
||||
// This should never happen as we ensure a consistent secret name.
|
||||
// But leave this in here just in case.
|
||||
klog.V(1).Infof("Duplicate bootstrap tokens found for id %s, ignoring on in %s/%s", tokenID, secret.Namespace, secret.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
// This secret looks good, add it to the list.
|
||||
ret[tokenID] = tokenSecret
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
|
@ -1,165 +0,0 @@
|
|||
/*
|
||||
Copyright 2016 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 bootstrap
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/informers"
|
||||
coreinformers "k8s.io/client-go/informers/core/v1"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
core "k8s.io/client-go/testing"
|
||||
bootstrapapi "k8s.io/cluster-bootstrap/token/api"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
"k8s.io/kubernetes/pkg/controller"
|
||||
)
|
||||
|
||||
func init() {
|
||||
spew.Config.DisableMethods = true
|
||||
}
|
||||
|
||||
const testTokenID = "abc123"
|
||||
|
||||
func newBootstrapSigner() (*BootstrapSigner, *fake.Clientset, coreinformers.SecretInformer, coreinformers.ConfigMapInformer, error) {
|
||||
options := DefaultBootstrapSignerOptions()
|
||||
cl := fake.NewSimpleClientset()
|
||||
informers := informers.NewSharedInformerFactory(fake.NewSimpleClientset(), controller.NoResyncPeriodFunc())
|
||||
secrets := informers.Core().V1().Secrets()
|
||||
configMaps := informers.Core().V1().ConfigMaps()
|
||||
bsc, err := NewBootstrapSigner(cl, secrets, configMaps, options)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
return bsc, cl, secrets, configMaps, nil
|
||||
}
|
||||
|
||||
func newConfigMap(tokenID, signature string) *v1.ConfigMap {
|
||||
ret := &v1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: metav1.NamespacePublic,
|
||||
Name: bootstrapapi.ConfigMapClusterInfo,
|
||||
ResourceVersion: "1",
|
||||
},
|
||||
Data: map[string]string{
|
||||
bootstrapapi.KubeConfigKey: "payload",
|
||||
},
|
||||
}
|
||||
if len(tokenID) > 0 {
|
||||
ret.Data[bootstrapapi.JWSSignatureKeyPrefix+tokenID] = signature
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func TestNoConfigMap(t *testing.T) {
|
||||
signer, cl, _, _, err := newBootstrapSigner()
|
||||
if err != nil {
|
||||
t.Fatalf("error creating BootstrapSigner: %v", err)
|
||||
}
|
||||
signer.signConfigMap()
|
||||
verifyActions(t, []core.Action{}, cl.Actions())
|
||||
}
|
||||
|
||||
func TestSimpleSign(t *testing.T) {
|
||||
signer, cl, secrets, configMaps, err := newBootstrapSigner()
|
||||
if err != nil {
|
||||
t.Fatalf("error creating BootstrapSigner: %v", err)
|
||||
}
|
||||
|
||||
cm := newConfigMap("", "")
|
||||
configMaps.Informer().GetIndexer().Add(cm)
|
||||
|
||||
secret := newTokenSecret(testTokenID, "tokenSecret")
|
||||
addSecretSigningUsage(secret, "true")
|
||||
secrets.Informer().GetIndexer().Add(secret)
|
||||
|
||||
signer.signConfigMap()
|
||||
|
||||
expected := []core.Action{
|
||||
core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "configmaps"},
|
||||
api.NamespacePublic,
|
||||
newConfigMap(testTokenID, "eyJhbGciOiJIUzI1NiIsImtpZCI6ImFiYzEyMyJ9..QSxpUG7Q542CirTI2ECPSZjvBOJURUW5a7XqFpNI958")),
|
||||
}
|
||||
|
||||
verifyActions(t, expected, cl.Actions())
|
||||
}
|
||||
|
||||
func TestNoSignNeeded(t *testing.T) {
|
||||
signer, cl, secrets, configMaps, err := newBootstrapSigner()
|
||||
if err != nil {
|
||||
t.Fatalf("error creating BootstrapSigner: %v", err)
|
||||
}
|
||||
|
||||
cm := newConfigMap(testTokenID, "eyJhbGciOiJIUzI1NiIsImtpZCI6ImFiYzEyMyJ9..QSxpUG7Q542CirTI2ECPSZjvBOJURUW5a7XqFpNI958")
|
||||
configMaps.Informer().GetIndexer().Add(cm)
|
||||
|
||||
secret := newTokenSecret(testTokenID, "tokenSecret")
|
||||
addSecretSigningUsage(secret, "true")
|
||||
secrets.Informer().GetIndexer().Add(secret)
|
||||
|
||||
signer.signConfigMap()
|
||||
|
||||
verifyActions(t, []core.Action{}, cl.Actions())
|
||||
}
|
||||
|
||||
func TestUpdateSignature(t *testing.T) {
|
||||
signer, cl, secrets, configMaps, err := newBootstrapSigner()
|
||||
if err != nil {
|
||||
t.Fatalf("error creating BootstrapSigner: %v", err)
|
||||
}
|
||||
|
||||
cm := newConfigMap(testTokenID, "old signature")
|
||||
configMaps.Informer().GetIndexer().Add(cm)
|
||||
|
||||
secret := newTokenSecret(testTokenID, "tokenSecret")
|
||||
addSecretSigningUsage(secret, "true")
|
||||
secrets.Informer().GetIndexer().Add(secret)
|
||||
|
||||
signer.signConfigMap()
|
||||
|
||||
expected := []core.Action{
|
||||
core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "configmaps"},
|
||||
api.NamespacePublic,
|
||||
newConfigMap(testTokenID, "eyJhbGciOiJIUzI1NiIsImtpZCI6ImFiYzEyMyJ9..QSxpUG7Q542CirTI2ECPSZjvBOJURUW5a7XqFpNI958")),
|
||||
}
|
||||
|
||||
verifyActions(t, expected, cl.Actions())
|
||||
}
|
||||
|
||||
func TestRemoveSignature(t *testing.T) {
|
||||
signer, cl, _, configMaps, err := newBootstrapSigner()
|
||||
if err != nil {
|
||||
t.Fatalf("error creating BootstrapSigner: %v", err)
|
||||
}
|
||||
|
||||
cm := newConfigMap(testTokenID, "old signature")
|
||||
configMaps.Informer().GetIndexer().Add(cm)
|
||||
|
||||
signer.signConfigMap()
|
||||
|
||||
expected := []core.Action{
|
||||
core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "configmaps"},
|
||||
api.NamespacePublic,
|
||||
newConfigMap("", "")),
|
||||
}
|
||||
|
||||
verifyActions(t, expected, cl.Actions())
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
/*
|
||||
Copyright 2016 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 bootstrap
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
core "k8s.io/client-go/testing"
|
||||
bootstrapapi "k8s.io/cluster-bootstrap/token/api"
|
||||
"k8s.io/kubernetes/pkg/apis/core/helper"
|
||||
)
|
||||
|
||||
func newTokenSecret(tokenID, tokenSecret string) *v1.Secret {
|
||||
return &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: metav1.NamespaceSystem,
|
||||
Name: bootstrapapi.BootstrapTokenSecretPrefix + tokenID,
|
||||
ResourceVersion: "1",
|
||||
},
|
||||
Type: bootstrapapi.SecretTypeBootstrapToken,
|
||||
Data: map[string][]byte{
|
||||
bootstrapapi.BootstrapTokenIDKey: []byte(tokenID),
|
||||
bootstrapapi.BootstrapTokenSecretKey: []byte(tokenSecret),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func addSecretExpiration(s *v1.Secret, expiration string) {
|
||||
s.Data[bootstrapapi.BootstrapTokenExpirationKey] = []byte(expiration)
|
||||
}
|
||||
|
||||
func addSecretSigningUsage(s *v1.Secret, value string) {
|
||||
s.Data[bootstrapapi.BootstrapTokenUsageSigningKey] = []byte(value)
|
||||
}
|
||||
|
||||
func verifyActions(t *testing.T, expected, actual []core.Action) {
|
||||
for i, a := range actual {
|
||||
if len(expected) < i+1 {
|
||||
t.Errorf("%d unexpected actions: %s", len(actual)-len(expected), spew.Sdump(actual[i:]))
|
||||
break
|
||||
}
|
||||
|
||||
e := expected[i]
|
||||
if !helper.Semantic.DeepEqual(e, a) {
|
||||
t.Errorf("Expected\n\t%s\ngot\n\t%s", spew.Sdump(e), spew.Sdump(a))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if len(expected) > len(actual) {
|
||||
t.Errorf("%d additional expected actions", len(expected)-len(actual))
|
||||
for _, a := range expected[len(actual):] {
|
||||
t.Logf(" %s", spew.Sdump(a))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
/*
|
||||
Copyright 2016 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 bootstrap provides automatic processes necessary for bootstraping.
|
||||
// This includes managing and expiring tokens along with signing well known
|
||||
// configmaps with those tokens.
|
||||
package bootstrap // import "k8s.io/kubernetes/pkg/controller/bootstrap"
|
|
@ -1,82 +0,0 @@
|
|||
/*
|
||||
Copyright 2016 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 bootstrap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
jose "gopkg.in/square/go-jose.v2"
|
||||
)
|
||||
|
||||
// computeDetachedSig takes content and token details and computes a detached
|
||||
// JWS signature. This is described in Appendix F of RFC 7515. Basically, this
|
||||
// is a regular JWS with the content part of the signature elided.
|
||||
func computeDetachedSig(content, tokenID, tokenSecret string) (string, error) {
|
||||
jwk := &jose.JSONWebKey{
|
||||
Key: []byte(tokenSecret),
|
||||
KeyID: tokenID,
|
||||
}
|
||||
|
||||
opts := &jose.SignerOptions{
|
||||
// Since this is a symmetric key, go-jose doesn't automatically include
|
||||
// the KeyID as part of the protected header. We have to pass it here
|
||||
// explicitly.
|
||||
ExtraHeaders: map[jose.HeaderKey]interface{}{
|
||||
"kid": tokenID,
|
||||
},
|
||||
}
|
||||
|
||||
signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.HS256, Key: jwk}, opts)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("can't make a HS256 signer from the given token: %v", err)
|
||||
}
|
||||
|
||||
jws, err := signer.Sign([]byte(content))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("can't HS256-sign the given token: %v", err)
|
||||
}
|
||||
|
||||
fullSig, err := jws.CompactSerialize()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("can't serialize the given token: %v", err)
|
||||
}
|
||||
return stripContent(fullSig)
|
||||
}
|
||||
|
||||
// stripContent will remove the content part of a compact JWS
|
||||
//
|
||||
// The `go-jose` library doesn't support generating signatures with "detached"
|
||||
// content. To make up for this we take the full compact signature, break it
|
||||
// apart and put it back together without the content section.
|
||||
func stripContent(fullSig string) (string, error) {
|
||||
parts := strings.Split(fullSig, ".")
|
||||
if len(parts) != 3 {
|
||||
return "", fmt.Errorf("compact JWS format must have three parts")
|
||||
}
|
||||
|
||||
return parts[0] + ".." + parts[2], nil
|
||||
}
|
||||
|
||||
// DetachedTokenIsValid checks whether a given detached JWS-encoded token matches JWS output of the given content and token
|
||||
func DetachedTokenIsValid(detachedToken, content, tokenID, tokenSecret string) bool {
|
||||
newToken, err := computeDetachedSig(content, tokenID, tokenSecret)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return detachedToken == newToken
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
/*
|
||||
Copyright 2016 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 bootstrap
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
content = "Hello from the other side. I must have called a thousand times."
|
||||
secret = "my voice is my passcode"
|
||||
id = "joshua"
|
||||
)
|
||||
|
||||
func TestComputeDetachedSig(t *testing.T) {
|
||||
sig, err := computeDetachedSig(content, id, secret)
|
||||
assert.NoError(t, err, "Error when computing signature: %v", err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"eyJhbGciOiJIUzI1NiIsImtpZCI6Impvc2h1YSJ9..VShe2taLd-YTrmWuRkcL_8QTNDHYxQIEBsAYYiIj1_8",
|
||||
sig,
|
||||
"Wrong signature. Got: %v", sig)
|
||||
|
||||
// Try with null content
|
||||
sig, err = computeDetachedSig("", id, secret)
|
||||
assert.NoError(t, err, "Error when computing signature: %v", err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"eyJhbGciOiJIUzI1NiIsImtpZCI6Impvc2h1YSJ9..7Ui1ALizW4jXphVUB7xUqC9vLYLL9RZeOFfVLoB7Tgk",
|
||||
sig,
|
||||
"Wrong signature. Got: %v", sig)
|
||||
|
||||
// Try with no secret
|
||||
sig, err = computeDetachedSig(content, id, "")
|
||||
assert.NoError(t, err, "Error when computing signature: %v", err)
|
||||
assert.Equal(
|
||||
t,
|
||||
"eyJhbGciOiJIUzI1NiIsImtpZCI6Impvc2h1YSJ9..UfkqvDGiIFxrMnFseDj9LYJOLNrvjW8aHhF71mvvAs8",
|
||||
sig,
|
||||
"Wrong signature. Got: %v", sig)
|
||||
}
|
||||
|
||||
func TestDetachedTokenIsValid(t *testing.T) {
|
||||
// Valid detached JWS token and valid inputs should succeed
|
||||
sig := "eyJhbGciOiJIUzI1NiIsImtpZCI6Impvc2h1YSJ9..VShe2taLd-YTrmWuRkcL_8QTNDHYxQIEBsAYYiIj1_8"
|
||||
assert.True(t, DetachedTokenIsValid(sig, content, id, secret),
|
||||
"Content %q and token \"%s:%s\" should equal signature: %q", content, id, secret, sig)
|
||||
|
||||
// Invalid detached JWS token and valid inputs should fail
|
||||
sig2 := sig + "foo"
|
||||
assert.False(t, DetachedTokenIsValid(sig2, content, id, secret),
|
||||
"Content %q and token \"%s:%s\" should not equal signature: %q", content, id, secret, sig)
|
||||
}
|
|
@ -1,203 +0,0 @@
|
|||
/*
|
||||
Copyright 2016 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 bootstrap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
coreinformers "k8s.io/client-go/informers/core/v1"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
corelisters "k8s.io/client-go/listers/core/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
bootstrapapi "k8s.io/cluster-bootstrap/token/api"
|
||||
"k8s.io/klog"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
"k8s.io/kubernetes/pkg/controller"
|
||||
"k8s.io/kubernetes/pkg/util/metrics"
|
||||
)
|
||||
|
||||
// TokenCleanerOptions contains options for the TokenCleaner
|
||||
type TokenCleanerOptions struct {
|
||||
// TokenSecretNamespace string is the namespace for token Secrets.
|
||||
TokenSecretNamespace string
|
||||
|
||||
// SecretResync is the time.Duration at which to fully re-list secrets.
|
||||
// If zero, re-list will be delayed as long as possible
|
||||
SecretResync time.Duration
|
||||
}
|
||||
|
||||
// DefaultTokenCleanerOptions returns a set of default options for creating a
|
||||
// TokenCleaner
|
||||
func DefaultTokenCleanerOptions() TokenCleanerOptions {
|
||||
return TokenCleanerOptions{
|
||||
TokenSecretNamespace: api.NamespaceSystem,
|
||||
}
|
||||
}
|
||||
|
||||
// TokenCleaner is a controller that deletes expired tokens
|
||||
type TokenCleaner struct {
|
||||
tokenSecretNamespace string
|
||||
|
||||
client clientset.Interface
|
||||
|
||||
// secretLister is able to list/get secrets and is populated by the shared informer passed to NewTokenCleaner.
|
||||
secretLister corelisters.SecretLister
|
||||
|
||||
// secretSynced returns true if the secret shared informer has been synced at least once.
|
||||
secretSynced cache.InformerSynced
|
||||
|
||||
queue workqueue.RateLimitingInterface
|
||||
}
|
||||
|
||||
// NewTokenCleaner returns a new *NewTokenCleaner.
|
||||
func NewTokenCleaner(cl clientset.Interface, secrets coreinformers.SecretInformer, options TokenCleanerOptions) (*TokenCleaner, error) {
|
||||
e := &TokenCleaner{
|
||||
client: cl,
|
||||
secretLister: secrets.Lister(),
|
||||
secretSynced: secrets.Informer().HasSynced,
|
||||
tokenSecretNamespace: options.TokenSecretNamespace,
|
||||
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "token_cleaner"),
|
||||
}
|
||||
|
||||
if cl.CoreV1().RESTClient().GetRateLimiter() != nil {
|
||||
if err := metrics.RegisterMetricAndTrackRateLimiterUsage("token_cleaner", cl.CoreV1().RESTClient().GetRateLimiter()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
secrets.Informer().AddEventHandlerWithResyncPeriod(
|
||||
cache.FilteringResourceEventHandler{
|
||||
FilterFunc: func(obj interface{}) bool {
|
||||
switch t := obj.(type) {
|
||||
case *v1.Secret:
|
||||
return t.Type == bootstrapapi.SecretTypeBootstrapToken && t.Namespace == e.tokenSecretNamespace
|
||||
default:
|
||||
utilruntime.HandleError(fmt.Errorf("object passed to %T that is not expected: %T", e, obj))
|
||||
return false
|
||||
}
|
||||
},
|
||||
Handler: cache.ResourceEventHandlerFuncs{
|
||||
AddFunc: e.enqueueSecrets,
|
||||
UpdateFunc: func(oldSecret, newSecret interface{}) { e.enqueueSecrets(newSecret) },
|
||||
},
|
||||
},
|
||||
options.SecretResync,
|
||||
)
|
||||
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// Run runs controller loops and returns when they are done
|
||||
func (tc *TokenCleaner) Run(stopCh <-chan struct{}) {
|
||||
defer utilruntime.HandleCrash()
|
||||
defer tc.queue.ShutDown()
|
||||
|
||||
klog.Infof("Starting token cleaner controller")
|
||||
defer klog.Infof("Shutting down token cleaner controller")
|
||||
|
||||
if !controller.WaitForCacheSync("token_cleaner", stopCh, tc.secretSynced) {
|
||||
return
|
||||
}
|
||||
|
||||
go wait.Until(tc.worker, 10*time.Second, stopCh)
|
||||
|
||||
<-stopCh
|
||||
}
|
||||
|
||||
func (tc *TokenCleaner) enqueueSecrets(obj interface{}) {
|
||||
key, err := controller.KeyFunc(obj)
|
||||
if err != nil {
|
||||
utilruntime.HandleError(err)
|
||||
return
|
||||
}
|
||||
tc.queue.Add(key)
|
||||
}
|
||||
|
||||
// worker runs a thread that dequeues secrets, handles them, and marks them done.
|
||||
func (tc *TokenCleaner) worker() {
|
||||
for tc.processNextWorkItem() {
|
||||
}
|
||||
}
|
||||
|
||||
// processNextWorkItem deals with one key off the queue. It returns false when it's time to quit.
|
||||
func (tc *TokenCleaner) processNextWorkItem() bool {
|
||||
key, quit := tc.queue.Get()
|
||||
if quit {
|
||||
return false
|
||||
}
|
||||
defer tc.queue.Done(key)
|
||||
|
||||
if err := tc.syncFunc(key.(string)); err != nil {
|
||||
tc.queue.AddRateLimited(key)
|
||||
utilruntime.HandleError(fmt.Errorf("Sync %v failed with : %v", key, err))
|
||||
return true
|
||||
}
|
||||
|
||||
tc.queue.Forget(key)
|
||||
return true
|
||||
}
|
||||
|
||||
func (tc *TokenCleaner) syncFunc(key string) error {
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
klog.V(4).Infof("Finished syncing secret %q (%v)", key, time.Since(startTime))
|
||||
}()
|
||||
|
||||
namespace, name, err := cache.SplitMetaNamespaceKey(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ret, err := tc.secretLister.Secrets(namespace).Get(name)
|
||||
if apierrors.IsNotFound(err) {
|
||||
klog.V(3).Infof("secret has been deleted: %v", key)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ret.Type == bootstrapapi.SecretTypeBootstrapToken {
|
||||
tc.evalSecret(ret)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tc *TokenCleaner) evalSecret(o interface{}) {
|
||||
secret := o.(*v1.Secret)
|
||||
if isSecretExpired(secret) {
|
||||
klog.V(3).Infof("Deleting expired secret %s/%s", secret.Namespace, secret.Name)
|
||||
var options *metav1.DeleteOptions
|
||||
if len(secret.UID) > 0 {
|
||||
options = &metav1.DeleteOptions{Preconditions: &metav1.Preconditions{UID: &secret.UID}}
|
||||
}
|
||||
err := tc.client.CoreV1().Secrets(secret.Namespace).Delete(secret.Name, options)
|
||||
// NotFound isn't a real error (it's already been deleted)
|
||||
// Conflict isn't a real error (the UID precondition failed)
|
||||
if err != nil && !apierrors.IsConflict(err) && !apierrors.IsNotFound(err) {
|
||||
klog.V(3).Infof("Error deleting Secret: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,102 +0,0 @@
|
|||
/*
|
||||
Copyright 2016 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 bootstrap
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/informers"
|
||||
coreinformers "k8s.io/client-go/informers/core/v1"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
core "k8s.io/client-go/testing"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
)
|
||||
|
||||
func init() {
|
||||
spew.Config.DisableMethods = true
|
||||
}
|
||||
|
||||
func newTokenCleaner() (*TokenCleaner, *fake.Clientset, coreinformers.SecretInformer, error) {
|
||||
options := DefaultTokenCleanerOptions()
|
||||
cl := fake.NewSimpleClientset()
|
||||
informerFactory := informers.NewSharedInformerFactory(cl, options.SecretResync)
|
||||
secrets := informerFactory.Core().V1().Secrets()
|
||||
tcc, err := NewTokenCleaner(cl, secrets, options)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
return tcc, cl, secrets, nil
|
||||
}
|
||||
|
||||
func TestCleanerNoExpiration(t *testing.T) {
|
||||
cleaner, cl, secrets, err := newTokenCleaner()
|
||||
if err != nil {
|
||||
t.Fatalf("error creating TokenCleaner: %v", err)
|
||||
}
|
||||
|
||||
secret := newTokenSecret("tokenID", "tokenSecret")
|
||||
secrets.Informer().GetIndexer().Add(secret)
|
||||
|
||||
cleaner.evalSecret(secret)
|
||||
|
||||
expected := []core.Action{}
|
||||
|
||||
verifyActions(t, expected, cl.Actions())
|
||||
}
|
||||
|
||||
func TestCleanerExpired(t *testing.T) {
|
||||
cleaner, cl, secrets, err := newTokenCleaner()
|
||||
if err != nil {
|
||||
t.Fatalf("error creating TokenCleaner: %v", err)
|
||||
}
|
||||
|
||||
secret := newTokenSecret("tokenID", "tokenSecret")
|
||||
addSecretExpiration(secret, timeString(-time.Hour))
|
||||
secrets.Informer().GetIndexer().Add(secret)
|
||||
|
||||
cleaner.evalSecret(secret)
|
||||
|
||||
expected := []core.Action{
|
||||
core.NewDeleteAction(
|
||||
schema.GroupVersionResource{Version: "v1", Resource: "secrets"},
|
||||
api.NamespaceSystem,
|
||||
secret.ObjectMeta.Name),
|
||||
}
|
||||
|
||||
verifyActions(t, expected, cl.Actions())
|
||||
}
|
||||
|
||||
func TestCleanerNotExpired(t *testing.T) {
|
||||
cleaner, cl, secrets, err := newTokenCleaner()
|
||||
if err != nil {
|
||||
t.Fatalf("error creating TokenCleaner: %v", err)
|
||||
}
|
||||
|
||||
secret := newTokenSecret("tokenID", "tokenSecret")
|
||||
addSecretExpiration(secret, timeString(time.Hour))
|
||||
secrets.Informer().GetIndexer().Add(secret)
|
||||
|
||||
cleaner.evalSecret(secret)
|
||||
|
||||
expected := []core.Action{}
|
||||
|
||||
verifyActions(t, expected, cl.Actions())
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
/*
|
||||
Copyright 2016 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 bootstrap
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"k8s.io/klog"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
bootstrapapi "k8s.io/cluster-bootstrap/token/api"
|
||||
)
|
||||
|
||||
var namePattern = `^` + regexp.QuoteMeta(bootstrapapi.BootstrapTokenSecretPrefix) + `([a-z0-9]{6})$`
|
||||
var nameRegExp = regexp.MustCompile(namePattern)
|
||||
|
||||
// getSecretString gets a string value from a secret. If there is an error or
|
||||
// if the key doesn't exist, an empty string is returned.
|
||||
func getSecretString(secret *v1.Secret, key string) string {
|
||||
data, ok := secret.Data[key]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
return string(data)
|
||||
}
|
||||
|
||||
// parseSecretName parses the name of the secret to extract the secret ID.
|
||||
func parseSecretName(name string) (secretID string, ok bool) {
|
||||
r := nameRegExp.FindStringSubmatch(name)
|
||||
if r == nil {
|
||||
return "", false
|
||||
}
|
||||
return r[1], true
|
||||
}
|
||||
|
||||
func validateSecretForSigning(secret *v1.Secret) (tokenID, tokenSecret string, ok bool) {
|
||||
nameTokenID, ok := parseSecretName(secret.Name)
|
||||
if !ok {
|
||||
klog.V(3).Infof("Invalid secret name: %s. Must be of form %s<secret-id>.", secret.Name, bootstrapapi.BootstrapTokenSecretPrefix)
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
tokenID = getSecretString(secret, bootstrapapi.BootstrapTokenIDKey)
|
||||
if len(tokenID) == 0 {
|
||||
klog.V(3).Infof("No %s key in %s/%s Secret", bootstrapapi.BootstrapTokenIDKey, secret.Namespace, secret.Name)
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
if nameTokenID != tokenID {
|
||||
klog.V(3).Infof("Token ID (%s) doesn't match secret name: %s", tokenID, nameTokenID)
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
tokenSecret = getSecretString(secret, bootstrapapi.BootstrapTokenSecretKey)
|
||||
if len(tokenSecret) == 0 {
|
||||
klog.V(3).Infof("No %s key in %s/%s Secret", bootstrapapi.BootstrapTokenSecretKey, secret.Namespace, secret.Name)
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
// Ensure this secret hasn't expired. The TokenCleaner should remove this
|
||||
// but if that isn't working or it hasn't gotten there yet we should check
|
||||
// here.
|
||||
if isSecretExpired(secret) {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
// Make sure this secret can be used for signing
|
||||
okToSign := getSecretString(secret, bootstrapapi.BootstrapTokenUsageSigningKey)
|
||||
if okToSign != "true" {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
return tokenID, tokenSecret, true
|
||||
}
|
||||
|
||||
// isSecretExpired returns true if the Secret is expired.
|
||||
func isSecretExpired(secret *v1.Secret) bool {
|
||||
expiration := getSecretString(secret, bootstrapapi.BootstrapTokenExpirationKey)
|
||||
if len(expiration) > 0 {
|
||||
expTime, err2 := time.Parse(time.RFC3339, expiration)
|
||||
if err2 != nil {
|
||||
klog.V(3).Infof("Unparseable expiration time (%s) in %s/%s Secret: %v. Treating as expired.",
|
||||
expiration, secret.Namespace, secret.Name, err2)
|
||||
return true
|
||||
}
|
||||
if time.Now().After(expTime) {
|
||||
klog.V(3).Infof("Expired bootstrap token in %s/%s Secret: %v",
|
||||
secret.Namespace, secret.Name, expiration)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
|
@ -1,206 +0,0 @@
|
|||
/*
|
||||
Copyright 2016 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 bootstrap
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
bootstrapapi "k8s.io/cluster-bootstrap/token/api"
|
||||
)
|
||||
|
||||
const (
|
||||
givenTokenID = "abc123"
|
||||
givenTokenID2 = "def456"
|
||||
givenTokenSecret = "tokenSecret"
|
||||
)
|
||||
|
||||
func timeString(delta time.Duration) string {
|
||||
return time.Now().Add(delta).Format(time.RFC3339)
|
||||
}
|
||||
|
||||
func TestValidateSecretForSigning(t *testing.T) {
|
||||
cases := []struct {
|
||||
description string
|
||||
tokenID string
|
||||
tokenSecret string
|
||||
okToSign string
|
||||
expiration string
|
||||
valid bool
|
||||
}{
|
||||
{
|
||||
"Signing token with no exp",
|
||||
givenTokenID, givenTokenSecret, "true", "", true,
|
||||
},
|
||||
{
|
||||
"Signing token with valid exp",
|
||||
givenTokenID, givenTokenSecret, "true", timeString(time.Hour), true,
|
||||
},
|
||||
{
|
||||
"Expired signing token",
|
||||
givenTokenID, givenTokenSecret, "true", timeString(-time.Hour), false,
|
||||
},
|
||||
{
|
||||
"Signing token with bad exp",
|
||||
givenTokenID, givenTokenSecret, "true", "garbage", false,
|
||||
},
|
||||
{
|
||||
"Signing token without signing bit",
|
||||
givenTokenID, givenTokenSecret, "", "garbage", false,
|
||||
},
|
||||
{
|
||||
"Signing token with bad signing bit",
|
||||
givenTokenID, givenTokenSecret, "", "", false,
|
||||
},
|
||||
{
|
||||
"Signing token with no ID",
|
||||
"", givenTokenSecret, "true", "", false,
|
||||
},
|
||||
{
|
||||
"Signing token with no secret",
|
||||
givenTokenID, "", "true", "", false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
secret := &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: metav1.NamespaceSystem,
|
||||
Name: bootstrapapi.BootstrapTokenSecretPrefix + givenTokenID,
|
||||
ResourceVersion: "1",
|
||||
},
|
||||
Type: bootstrapapi.SecretTypeBootstrapToken,
|
||||
Data: map[string][]byte{
|
||||
bootstrapapi.BootstrapTokenIDKey: []byte(tc.tokenID),
|
||||
bootstrapapi.BootstrapTokenSecretKey: []byte(tc.tokenSecret),
|
||||
bootstrapapi.BootstrapTokenUsageSigningKey: []byte(tc.okToSign),
|
||||
bootstrapapi.BootstrapTokenExpirationKey: []byte(tc.expiration),
|
||||
},
|
||||
}
|
||||
|
||||
tokenID, tokenSecret, ok := validateSecretForSigning(secret)
|
||||
if ok != tc.valid {
|
||||
t.Errorf("%s: Unexpected validation failure. Expected %v, got %v", tc.description, tc.valid, ok)
|
||||
}
|
||||
if ok {
|
||||
if tokenID != tc.tokenID {
|
||||
t.Errorf("%s: Unexpected Token ID. Expected %q, got %q", tc.description, givenTokenID, tokenID)
|
||||
}
|
||||
if tokenSecret != tc.tokenSecret {
|
||||
t.Errorf("%s: Unexpected Token Secret. Expected %q, got %q", tc.description, givenTokenSecret, tokenSecret)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestValidateSecret(t *testing.T) {
|
||||
secret := &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: metav1.NamespaceSystem,
|
||||
Name: bootstrapapi.BootstrapTokenSecretPrefix + givenTokenID,
|
||||
ResourceVersion: "1",
|
||||
},
|
||||
Type: bootstrapapi.SecretTypeBootstrapToken,
|
||||
Data: map[string][]byte{
|
||||
bootstrapapi.BootstrapTokenIDKey: []byte(givenTokenID),
|
||||
bootstrapapi.BootstrapTokenSecretKey: []byte(givenTokenSecret),
|
||||
bootstrapapi.BootstrapTokenUsageSigningKey: []byte("true"),
|
||||
},
|
||||
}
|
||||
|
||||
tokenID, tokenSecret, ok := validateSecretForSigning(secret)
|
||||
if !ok {
|
||||
t.Errorf("Unexpected validation failure.")
|
||||
}
|
||||
if tokenID != givenTokenID {
|
||||
t.Errorf("Unexpected Token ID. Expected %q, got %q", givenTokenID, tokenID)
|
||||
}
|
||||
if tokenSecret != givenTokenSecret {
|
||||
t.Errorf("Unexpected Token Secret. Expected %q, got %q", givenTokenSecret, tokenSecret)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBadSecretName(t *testing.T) {
|
||||
secret := &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: metav1.NamespaceSystem,
|
||||
Name: givenTokenID,
|
||||
ResourceVersion: "1",
|
||||
},
|
||||
Type: bootstrapapi.SecretTypeBootstrapToken,
|
||||
Data: map[string][]byte{
|
||||
bootstrapapi.BootstrapTokenIDKey: []byte(givenTokenID),
|
||||
bootstrapapi.BootstrapTokenSecretKey: []byte(givenTokenSecret),
|
||||
bootstrapapi.BootstrapTokenUsageSigningKey: []byte("true"),
|
||||
},
|
||||
}
|
||||
|
||||
_, _, ok := validateSecretForSigning(secret)
|
||||
if ok {
|
||||
t.Errorf("Token validation should fail with bad name")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMismatchSecretName(t *testing.T) {
|
||||
secret := &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: metav1.NamespaceSystem,
|
||||
Name: bootstrapapi.BootstrapTokenSecretPrefix + givenTokenID2,
|
||||
ResourceVersion: "1",
|
||||
},
|
||||
Type: bootstrapapi.SecretTypeBootstrapToken,
|
||||
Data: map[string][]byte{
|
||||
bootstrapapi.BootstrapTokenIDKey: []byte(givenTokenID),
|
||||
bootstrapapi.BootstrapTokenSecretKey: []byte(givenTokenSecret),
|
||||
bootstrapapi.BootstrapTokenUsageSigningKey: []byte("true"),
|
||||
},
|
||||
}
|
||||
|
||||
_, _, ok := validateSecretForSigning(secret)
|
||||
if ok {
|
||||
t.Errorf("Token validation should fail with mismatched name")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSecretName(t *testing.T) {
|
||||
tokenID, ok := parseSecretName("bootstrap-token-abc123")
|
||||
assert.True(t, ok, "parseSecretName should accept valid name")
|
||||
assert.Equal(t, "abc123", tokenID, "parseSecretName should return token ID")
|
||||
|
||||
_, ok = parseSecretName("")
|
||||
assert.False(t, ok, "parseSecretName should reject blank name")
|
||||
|
||||
_, ok = parseSecretName("abc123")
|
||||
assert.False(t, ok, "parseSecretName should reject with no prefix")
|
||||
|
||||
_, ok = parseSecretName("bootstrap-token-")
|
||||
assert.False(t, ok, "parseSecretName should reject no token ID")
|
||||
|
||||
_, ok = parseSecretName("bootstrap-token-abc")
|
||||
assert.False(t, ok, "parseSecretName should reject short token ID")
|
||||
|
||||
_, ok = parseSecretName("bootstrap-token-abc123ghi")
|
||||
assert.False(t, ok, "parseSecretName should reject long token ID")
|
||||
|
||||
_, ok = parseSecretName("bootstrap-token-ABC123")
|
||||
assert.False(t, ok, "parseSecretName should reject invalid token ID")
|
||||
}
|
Loading…
Reference in New Issue