mirror of
				https://github.com/optim-enterprises-bv/kubernetes.git
				synced 2025-10-31 18:28:13 +00:00 
			
		
		
		
	Introduces BootstrapSigner controller
This commit is contained in:
		| @@ -84,6 +84,7 @@ filegroup( | |||||||
|     name = "all-srcs", |     name = "all-srcs", | ||||||
|     srcs = [ |     srcs = [ | ||||||
|         ":package-srcs", |         ":package-srcs", | ||||||
|  |         "//pkg/controller/bootstrap:all-srcs", | ||||||
|         "//pkg/controller/certificates:all-srcs", |         "//pkg/controller/certificates:all-srcs", | ||||||
|         "//pkg/controller/cloud:all-srcs", |         "//pkg/controller/cloud:all-srcs", | ||||||
|         "//pkg/controller/cronjob:all-srcs", |         "//pkg/controller/cronjob:all-srcs", | ||||||
|   | |||||||
							
								
								
									
										73
									
								
								pkg/controller/bootstrap/BUILD
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								pkg/controller/bootstrap/BUILD
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | |||||||
|  | package(default_visibility = ["//visibility:public"]) | ||||||
|  |  | ||||||
|  | licenses(["notice"]) | ||||||
|  |  | ||||||
|  | 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", | ||||||
|  |         "util_test.go", | ||||||
|  |     ], | ||||||
|  |     library = ":go_default_library", | ||||||
|  |     tags = ["automanaged"], | ||||||
|  |     deps = [ | ||||||
|  |         "//pkg/bootstrap/api:go_default_library", | ||||||
|  |         "//vendor:github.com/davecgh/go-spew/spew", | ||||||
|  |         "//vendor:k8s.io/apimachinery/pkg/apis/meta/v1", | ||||||
|  |         "//vendor:k8s.io/apimachinery/pkg/runtime/schema", | ||||||
|  |         "//vendor:k8s.io/client-go/kubernetes/fake", | ||||||
|  |         "//vendor:k8s.io/client-go/pkg/api", | ||||||
|  |         "//vendor:k8s.io/client-go/pkg/api/v1", | ||||||
|  |         "//vendor:k8s.io/client-go/testing", | ||||||
|  |     ], | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | go_library( | ||||||
|  |     name = "go_default_library", | ||||||
|  |     srcs = [ | ||||||
|  |         "bootstrapsigner.go", | ||||||
|  |         "doc.go", | ||||||
|  |         "jws.go", | ||||||
|  |         "util.go", | ||||||
|  |     ], | ||||||
|  |     tags = ["automanaged"], | ||||||
|  |     deps = [ | ||||||
|  |         "//pkg/bootstrap/api:go_default_library", | ||||||
|  |         "//pkg/util/metrics:go_default_library", | ||||||
|  |         "//vendor:github.com/golang/glog", | ||||||
|  |         "//vendor:github.com/square/go-jose", | ||||||
|  |         "//vendor:k8s.io/apimachinery/pkg/api/errors", | ||||||
|  |         "//vendor:k8s.io/apimachinery/pkg/apis/meta/v1", | ||||||
|  |         "//vendor:k8s.io/apimachinery/pkg/fields", | ||||||
|  |         "//vendor:k8s.io/apimachinery/pkg/runtime", | ||||||
|  |         "//vendor:k8s.io/apimachinery/pkg/util/runtime", | ||||||
|  |         "//vendor:k8s.io/apimachinery/pkg/util/wait", | ||||||
|  |         "//vendor:k8s.io/apimachinery/pkg/watch", | ||||||
|  |         "//vendor:k8s.io/client-go/kubernetes", | ||||||
|  |         "//vendor:k8s.io/client-go/pkg/api", | ||||||
|  |         "//vendor:k8s.io/client-go/pkg/api/v1", | ||||||
|  |         "//vendor:k8s.io/client-go/tools/cache", | ||||||
|  |         "//vendor:k8s.io/client-go/util/workqueue", | ||||||
|  |     ], | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | filegroup( | ||||||
|  |     name = "package-srcs", | ||||||
|  |     srcs = glob(["**"]), | ||||||
|  |     tags = ["automanaged"], | ||||||
|  |     visibility = ["//visibility:private"], | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | filegroup( | ||||||
|  |     name = "all-srcs", | ||||||
|  |     srcs = [":package-srcs"], | ||||||
|  |     tags = ["automanaged"], | ||||||
|  | ) | ||||||
							
								
								
									
										303
									
								
								pkg/controller/bootstrap/bootstrapsigner.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										303
									
								
								pkg/controller/bootstrap/bootstrapsigner.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,303 @@ | |||||||
|  | /* | ||||||
|  | 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" | ||||||
|  |  | ||||||
|  | 	"github.com/golang/glog" | ||||||
|  |  | ||||||
|  | 	apierrors "k8s.io/apimachinery/pkg/api/errors" | ||||||
|  | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
|  | 	"k8s.io/apimachinery/pkg/fields" | ||||||
|  | 	"k8s.io/apimachinery/pkg/runtime" | ||||||
|  | 	utilruntime "k8s.io/apimachinery/pkg/util/runtime" | ||||||
|  | 	"k8s.io/apimachinery/pkg/util/wait" | ||||||
|  | 	"k8s.io/apimachinery/pkg/watch" | ||||||
|  | 	clientset "k8s.io/client-go/kubernetes" | ||||||
|  | 	"k8s.io/client-go/pkg/api" | ||||||
|  | 	"k8s.io/client-go/pkg/api/v1" | ||||||
|  | 	"k8s.io/client-go/tools/cache" | ||||||
|  | 	"k8s.io/client-go/util/workqueue" | ||||||
|  | 	bootstrapapi "k8s.io/kubernetes/pkg/bootstrap/api" | ||||||
|  | 	"k8s.io/kubernetes/pkg/util/metrics" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	configMapClusterInfo = "cluster-info" | ||||||
|  | 	kubeConfigKey        = "kubeconfig" | ||||||
|  | 	signaturePrefix      = "jws-kubeconfig-" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // 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:        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 | ||||||
|  | 	secretNamespace string | ||||||
|  |  | ||||||
|  | 	configMaps cache.Store | ||||||
|  | 	secrets    cache.Store | ||||||
|  |  | ||||||
|  | 	// 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.Interface | ||||||
|  |  | ||||||
|  | 	// Since we join two objects, we'll watch both of them with controllers. | ||||||
|  | 	configMapsController cache.Controller | ||||||
|  | 	secretsController    cache.Controller | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NewBootstrapSigner returns a new *BootstrapSigner. | ||||||
|  | // | ||||||
|  | // TODO: Switch to shared informers | ||||||
|  | func NewBootstrapSigner(cl clientset.Interface, options BootstrapSignerOptions) *BootstrapSigner { | ||||||
|  | 	e := &BootstrapSigner{ | ||||||
|  | 		client:          cl, | ||||||
|  | 		configMapKey:    options.ConfigMapNamespace + "/" + options.ConfigMapName, | ||||||
|  | 		secretNamespace: options.TokenSecretNamespace, | ||||||
|  | 		syncQueue:       workqueue.NewNamed("bootstrap_signer_queue"), | ||||||
|  | 	} | ||||||
|  | 	if cl.Core().RESTClient().GetRateLimiter() != nil { | ||||||
|  | 		metrics.RegisterMetricAndTrackRateLimiterUsage("bootstrap_signer", cl.Core().RESTClient().GetRateLimiter()) | ||||||
|  | 	} | ||||||
|  | 	configMapSelector := fields.SelectorFromSet(map[string]string{api.ObjectNameField: options.ConfigMapName}) | ||||||
|  | 	e.configMaps, e.configMapsController = cache.NewInformer( | ||||||
|  | 		&cache.ListWatch{ | ||||||
|  | 			ListFunc: func(lo metav1.ListOptions) (runtime.Object, error) { | ||||||
|  | 				lo.FieldSelector = configMapSelector.String() | ||||||
|  | 				return e.client.Core().ConfigMaps(options.ConfigMapNamespace).List(lo) | ||||||
|  | 			}, | ||||||
|  | 			WatchFunc: func(lo metav1.ListOptions) (watch.Interface, error) { | ||||||
|  | 				lo.FieldSelector = configMapSelector.String() | ||||||
|  | 				return e.client.Core().ConfigMaps(options.ConfigMapNamespace).Watch(lo) | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		&v1.ConfigMap{}, | ||||||
|  | 		options.ConfigMapResync, | ||||||
|  | 		cache.ResourceEventHandlerFuncs{ | ||||||
|  | 			AddFunc:    func(_ interface{}) { e.pokeConfigMapSync() }, | ||||||
|  | 			UpdateFunc: func(_, _ interface{}) { e.pokeConfigMapSync() }, | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  |  | ||||||
|  | 	secretSelector := fields.SelectorFromSet(map[string]string{api.SecretTypeField: string(bootstrapapi.SecretTypeBootstrapToken)}) | ||||||
|  | 	e.secrets, e.secretsController = cache.NewInformer( | ||||||
|  | 		&cache.ListWatch{ | ||||||
|  | 			ListFunc: func(lo metav1.ListOptions) (runtime.Object, error) { | ||||||
|  | 				lo.FieldSelector = secretSelector.String() | ||||||
|  | 				return e.client.Core().Secrets(e.secretNamespace).List(lo) | ||||||
|  | 			}, | ||||||
|  | 			WatchFunc: func(lo metav1.ListOptions) (watch.Interface, error) { | ||||||
|  | 				lo.FieldSelector = secretSelector.String() | ||||||
|  | 				return e.client.Core().Secrets(e.secretNamespace).Watch(lo) | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		&v1.Secret{}, | ||||||
|  | 		options.SecretResync, | ||||||
|  | 		cache.ResourceEventHandlerFuncs{ | ||||||
|  | 			AddFunc:    func(_ interface{}) { e.pokeConfigMapSync() }, | ||||||
|  | 			UpdateFunc: func(_, _ interface{}) { e.pokeConfigMapSync() }, | ||||||
|  | 			DeleteFunc: func(_ interface{}) { e.pokeConfigMapSync() }, | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | 	return e | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Run runs controller loops and returns when they are done | ||||||
|  | func (e *BootstrapSigner) Run(stopCh <-chan struct{}) { | ||||||
|  | 	go e.configMapsController.Run(stopCh) | ||||||
|  | 	go e.secretsController.Run(stopCh) | ||||||
|  | 	go wait.Until(e.serviceConfigMapQueue, 0, stopCh) | ||||||
|  | 	<-stopCh | ||||||
|  | } | ||||||
|  |  | ||||||
|  | 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, err := copyConfigMap(origCM) | ||||||
|  | 	if err != nil { | ||||||
|  | 		utilruntime.HandleError(err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// First capture the config we are signing | ||||||
|  | 	content, ok := newCM.Data[kubeConfigKey] | ||||||
|  | 	if !ok { | ||||||
|  | 		glog.V(3).Infof("No %s key in %s/%s ConfigMap", 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, signaturePrefix) { | ||||||
|  | 			tokenID := strings.TrimPrefix(key, signaturePrefix) | ||||||
|  | 			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[signaturePrefix+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.Core().ConfigMaps(cm.Namespace).Update(cm) | ||||||
|  | 	if err != nil && !apierrors.IsConflict(err) && !apierrors.IsNotFound(err) { | ||||||
|  | 		glog.V(3).Infof("Error updating ConfigMap: %v", err) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // getConfigMap gets the ConfigMap we are interested in | ||||||
|  | func (e *BootstrapSigner) getConfigMap() *v1.ConfigMap { | ||||||
|  | 	configMap, exists, err := e.configMaps.GetByKey(e.configMapKey) | ||||||
|  |  | ||||||
|  | 	// If we can't get the configmap just return nil. The resync will eventually | ||||||
|  | 	// sync things up. | ||||||
|  | 	if err != nil { | ||||||
|  | 		utilruntime.HandleError(err) | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if exists { | ||||||
|  | 		return configMap.(*v1.ConfigMap) | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (e *BootstrapSigner) listSecrets() []*v1.Secret { | ||||||
|  | 	secrets := e.secrets.List() | ||||||
|  |  | ||||||
|  | 	items := []*v1.Secret{} | ||||||
|  | 	for _, obj := range secrets { | ||||||
|  | 		items = append(items, obj.(*v1.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 { | ||||||
|  | 			glog.V(3).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 | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func copyConfigMap(orig *v1.ConfigMap) (*v1.ConfigMap, error) { | ||||||
|  | 	newCMObj, err := api.Scheme.DeepCopy(orig) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return newCMObj.(*v1.ConfigMap), nil | ||||||
|  | } | ||||||
							
								
								
									
										137
									
								
								pkg/controller/bootstrap/bootstrapsigner_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								pkg/controller/bootstrap/bootstrapsigner_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,137 @@ | |||||||
|  | /* | ||||||
|  | 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" | ||||||
|  |  | ||||||
|  | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
|  | 	"k8s.io/apimachinery/pkg/runtime/schema" | ||||||
|  | 	"k8s.io/client-go/kubernetes/fake" | ||||||
|  | 	"k8s.io/client-go/pkg/api" | ||||||
|  | 	"k8s.io/client-go/pkg/api/v1" | ||||||
|  | 	core "k8s.io/client-go/testing" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	spew.Config.DisableMethods = true | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func newBootstrapSigner() (*BootstrapSigner, *fake.Clientset) { | ||||||
|  | 	options := DefaultBootstrapSignerOptions() | ||||||
|  | 	cl := fake.NewSimpleClientset() | ||||||
|  | 	return NewBootstrapSigner(cl, options), cl | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func newConfigMap(tokenID, signature string) *v1.ConfigMap { | ||||||
|  | 	ret := &v1.ConfigMap{ | ||||||
|  | 		ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 			Namespace:       metav1.NamespacePublic, | ||||||
|  | 			Name:            configMapClusterInfo, | ||||||
|  | 			ResourceVersion: "1", | ||||||
|  | 		}, | ||||||
|  | 		Data: map[string]string{ | ||||||
|  | 			kubeConfigKey: "payload", | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	if len(tokenID) > 0 { | ||||||
|  | 		ret.Data[signaturePrefix+tokenID] = signature | ||||||
|  | 	} | ||||||
|  | 	return ret | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestNoConfigMap(t *testing.T) { | ||||||
|  | 	signer, cl := newBootstrapSigner() | ||||||
|  | 	signer.signConfigMap() | ||||||
|  | 	verifyActions(t, []core.Action{}, cl.Actions()) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestSimpleSign(t *testing.T) { | ||||||
|  | 	signer, cl := newBootstrapSigner() | ||||||
|  |  | ||||||
|  | 	cm := newConfigMap("", "") | ||||||
|  | 	signer.configMaps.Add(cm) | ||||||
|  |  | ||||||
|  | 	secret := newTokenSecret("tokenID", "tokenSecret") | ||||||
|  | 	addSecretSigningUsage(secret, "true") | ||||||
|  | 	signer.secrets.Add(secret) | ||||||
|  |  | ||||||
|  | 	signer.signConfigMap() | ||||||
|  |  | ||||||
|  | 	expected := []core.Action{ | ||||||
|  | 		core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}, | ||||||
|  | 			api.NamespacePublic, | ||||||
|  | 			newConfigMap("tokenID", "eyJhbGciOiJIUzI1NiIsImtpZCI6InRva2VuSUQifQ..QAvK9DAjF0hSyASEkH1MOTB5rJMmbWEY9j-z1NSYILE")), | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	verifyActions(t, expected, cl.Actions()) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestNoSignNeeded(t *testing.T) { | ||||||
|  | 	signer, cl := newBootstrapSigner() | ||||||
|  |  | ||||||
|  | 	cm := newConfigMap("tokenID", "eyJhbGciOiJIUzI1NiIsImtpZCI6InRva2VuSUQifQ..QAvK9DAjF0hSyASEkH1MOTB5rJMmbWEY9j-z1NSYILE") | ||||||
|  | 	signer.configMaps.Add(cm) | ||||||
|  |  | ||||||
|  | 	secret := newTokenSecret("tokenID", "tokenSecret") | ||||||
|  | 	addSecretSigningUsage(secret, "true") | ||||||
|  | 	signer.secrets.Add(secret) | ||||||
|  |  | ||||||
|  | 	signer.signConfigMap() | ||||||
|  |  | ||||||
|  | 	verifyActions(t, []core.Action{}, cl.Actions()) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestUpdateSignature(t *testing.T) { | ||||||
|  | 	signer, cl := newBootstrapSigner() | ||||||
|  |  | ||||||
|  | 	cm := newConfigMap("tokenID", "old signature") | ||||||
|  | 	signer.configMaps.Add(cm) | ||||||
|  |  | ||||||
|  | 	secret := newTokenSecret("tokenID", "tokenSecret") | ||||||
|  | 	addSecretSigningUsage(secret, "true") | ||||||
|  | 	signer.secrets.Add(secret) | ||||||
|  |  | ||||||
|  | 	signer.signConfigMap() | ||||||
|  |  | ||||||
|  | 	expected := []core.Action{ | ||||||
|  | 		core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}, | ||||||
|  | 			api.NamespacePublic, | ||||||
|  | 			newConfigMap("tokenID", "eyJhbGciOiJIUzI1NiIsImtpZCI6InRva2VuSUQifQ..QAvK9DAjF0hSyASEkH1MOTB5rJMmbWEY9j-z1NSYILE")), | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	verifyActions(t, expected, cl.Actions()) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestRemoveSignature(t *testing.T) { | ||||||
|  | 	signer, cl := newBootstrapSigner() | ||||||
|  |  | ||||||
|  | 	cm := newConfigMap("tokenID", "old signature") | ||||||
|  | 	signer.configMaps.Add(cm) | ||||||
|  |  | ||||||
|  | 	signer.signConfigMap() | ||||||
|  |  | ||||||
|  | 	expected := []core.Action{ | ||||||
|  | 		core.NewUpdateAction(schema.GroupVersionResource{Version: "v1", Resource: "configmaps"}, | ||||||
|  | 			api.NamespacePublic, | ||||||
|  | 			newConfigMap("", "")), | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	verifyActions(t, expected, cl.Actions()) | ||||||
|  | } | ||||||
							
								
								
									
										74
									
								
								pkg/controller/bootstrap/common_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								pkg/controller/bootstrap/common_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | |||||||
|  | /* | ||||||
|  | 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" | ||||||
|  |  | ||||||
|  | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
|  | 	"k8s.io/client-go/pkg/api" | ||||||
|  | 	"k8s.io/client-go/pkg/api/v1" | ||||||
|  | 	core "k8s.io/client-go/testing" | ||||||
|  | 	bootstrapapi "k8s.io/kubernetes/pkg/bootstrap/api" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func newTokenSecret(tokenID, tokenSecret string) *v1.Secret { | ||||||
|  | 	return &v1.Secret{ | ||||||
|  | 		ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 			Namespace:       metav1.NamespaceSystem, | ||||||
|  | 			Name:            "secretName", | ||||||
|  | 			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 !api.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)) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										20
									
								
								pkg/controller/bootstrap/doc.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								pkg/controller/bootstrap/doc.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | /* | ||||||
|  | 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" | ||||||
							
								
								
									
										64
									
								
								pkg/controller/bootstrap/jws.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								pkg/controller/bootstrap/jws.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | |||||||
|  | /* | ||||||
|  | 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 "github.com/square/go-jose" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // 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, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	signer, err := jose.NewSigner(jose.HS256, jwk) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	jws, err := signer.Sign([]byte(content)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	fullSig, err := jws.CompactSerialize() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return "", nil | ||||||
|  | 	} | ||||||
|  | 	return stripContent(fullSig) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // stripContent will remove the content part of a compact JWS | ||||||
|  | // | ||||||
|  | // The `go-jose` library doesn't support generating signatures with "detatched" | ||||||
|  | // 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 | ||||||
|  | } | ||||||
							
								
								
									
										53
									
								
								pkg/controller/bootstrap/jws_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								pkg/controller/bootstrap/jws_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | |||||||
|  | /* | ||||||
|  | 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" | ||||||
|  |  | ||||||
|  | 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) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Errorf("Error when computing signature: %v", err) | ||||||
|  | 	} | ||||||
|  | 	if sig != "eyJhbGciOiJIUzI1NiIsImtpZCI6Impvc2h1YSJ9..VShe2taLd-YTrmWuRkcL_8QTNDHYxQIEBsAYYiIj1_8" { | ||||||
|  | 		t.Errorf("Wrong signature. Got: %v", sig) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Try with null content | ||||||
|  | 	sig, err = computeDetachedSig("", id, secret) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Errorf("Error when computing signature: %v", err) | ||||||
|  | 	} | ||||||
|  | 	if sig != "eyJhbGciOiJIUzI1NiIsImtpZCI6Impvc2h1YSJ9..7Ui1ALizW4jXphVUB7xUqC9vLYLL9RZeOFfVLoB7Tgk" { | ||||||
|  | 		t.Errorf("Wrong signature. Got: %v", sig) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Try with no secret | ||||||
|  | 	sig, err = computeDetachedSig(content, id, "") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Errorf("Error when computing signature: %v", err) | ||||||
|  | 	} | ||||||
|  | 	if sig != "eyJhbGciOiJIUzI1NiIsImtpZCI6Impvc2h1YSJ9..UfkqvDGiIFxrMnFseDj9LYJOLNrvjW8aHhF71mvvAs8" { | ||||||
|  | 		t.Errorf("Wrong signature. Got: %v", sig) | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										85
									
								
								pkg/controller/bootstrap/util.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								pkg/controller/bootstrap/util.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,85 @@ | |||||||
|  | /* | ||||||
|  | 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 ( | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/golang/glog" | ||||||
|  |  | ||||||
|  | 	"k8s.io/client-go/pkg/api/v1" | ||||||
|  | 	bootstrapapi "k8s.io/kubernetes/pkg/bootstrap/api" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // 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) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func validateSecretForSigning(secret *v1.Secret) (tokenID, tokenSecret string, ok bool) { | ||||||
|  | 	tokenID = getSecretString(secret, bootstrapapi.BootstrapTokenIDKey) | ||||||
|  | 	if len(tokenID) == 0 { | ||||||
|  | 		glog.V(3).Infof("No %s key in %s/%s Secret", bootstrapapi.BootstrapTokenIDKey, secret.Namespace, secret.Name) | ||||||
|  | 		return "", "", false | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	tokenSecret = getSecretString(secret, bootstrapapi.BootstrapTokenSecretKey) | ||||||
|  | 	if len(tokenSecret) == 0 { | ||||||
|  | 		glog.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 { | ||||||
|  | 			glog.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) { | ||||||
|  | 			glog.V(3).Infof("Expired bootstrap token in %s/%s Secret: %v", | ||||||
|  | 				secret.Namespace, secret.Name, expiration) | ||||||
|  | 			return true | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
							
								
								
									
										137
									
								
								pkg/controller/bootstrap/util_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								pkg/controller/bootstrap/util_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,137 @@ | |||||||
|  | /* | ||||||
|  | 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" | ||||||
|  |  | ||||||
|  | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
|  | 	"k8s.io/client-go/pkg/api/v1" | ||||||
|  | 	bootstrapapi "k8s.io/kubernetes/pkg/bootstrap/api" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	givenTokenID     = "tokenID" | ||||||
|  | 	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:            "secretName", | ||||||
|  | 				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:            "secretName", | ||||||
|  | 			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) | ||||||
|  | 	} | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user
	 Joe Beda
					Joe Beda