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", | ||||
|     srcs = [ | ||||
|         ":package-srcs", | ||||
|         "//pkg/controller/bootstrap:all-srcs", | ||||
|         "//pkg/controller/certificates:all-srcs", | ||||
|         "//pkg/controller/cloud: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