From 2ee7db4daf64f3dc281e8ff16b43493615668377 Mon Sep 17 00:00:00 2001 From: Joe Beda Date: Tue, 1 Nov 2016 07:01:32 -0700 Subject: [PATCH] Introduce TokenCleaner to clean out expired bootstrap tokens --- pkg/controller/bootstrap/BUILD | 2 + pkg/controller/bootstrap/tokencleaner.go | 118 ++++++++++++++++++ pkg/controller/bootstrap/tokencleaner_test.go | 85 +++++++++++++ 3 files changed, 205 insertions(+) create mode 100644 pkg/controller/bootstrap/tokencleaner.go create mode 100644 pkg/controller/bootstrap/tokencleaner_test.go diff --git a/pkg/controller/bootstrap/BUILD b/pkg/controller/bootstrap/BUILD index b7dd4c70f93..185e348b65c 100644 --- a/pkg/controller/bootstrap/BUILD +++ b/pkg/controller/bootstrap/BUILD @@ -14,6 +14,7 @@ go_test( "bootstrapsigner_test.go", "common_test.go", "jws_test.go", + "tokencleaner_test.go", "util_test.go", ], library = ":go_default_library", @@ -36,6 +37,7 @@ go_library( "bootstrapsigner.go", "doc.go", "jws.go", + "tokencleaner.go", "util.go", ], tags = ["automanaged"], diff --git a/pkg/controller/bootstrap/tokencleaner.go b/pkg/controller/bootstrap/tokencleaner.go new file mode 100644 index 00000000000..dbab79c4b90 --- /dev/null +++ b/pkg/controller/bootstrap/tokencleaner.go @@ -0,0 +1,118 @@ +/* +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" + + 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" + "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" + bootstrapapi "k8s.io/kubernetes/pkg/bootstrap/api" + "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{} +} + +// TokenCleaner is a controller that deletes expired tokens +type TokenCleaner struct { + tokenSecretNamespace string + + client clientset.Interface + + secrets cache.Store + secretsController cache.Controller +} + +// NewTokenCleaner returns a new *NewTokenCleaner. +// +// TODO: Switch to shared informers +func NewTokenCleaner(cl clientset.Interface, options TokenCleanerOptions) *TokenCleaner { + e := &TokenCleaner{ + client: cl, + tokenSecretNamespace: options.TokenSecretNamespace, + } + if cl.Core().RESTClient().GetRateLimiter() != nil { + metrics.RegisterMetricAndTrackRateLimiterUsage("token_cleaner", cl.Core().RESTClient().GetRateLimiter()) + } + + 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.tokenSecretNamespace).List(lo) + }, + WatchFunc: func(lo metav1.ListOptions) (watch.Interface, error) { + lo.FieldSelector = secretSelector.String() + return e.client.Core().Secrets(e.tokenSecretNamespace).Watch(lo) + }, + }, + &v1.Secret{}, + options.SecretResync, + cache.ResourceEventHandlerFuncs{ + AddFunc: e.evalSecret, + UpdateFunc: func(oldSecret, newSecret interface{}) { e.evalSecret(newSecret) }, + }, + ) + return e +} + +// Run runs controller loops and returns when they are done +func (tc *TokenCleaner) Run(stopCh <-chan struct{}) { + go tc.secretsController.Run(stopCh) + <-stopCh +} + +func (tc *TokenCleaner) evalSecret(o interface{}) { + secret := o.(*v1.Secret) + if isSecretExpired(secret) { + glog.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.Core().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) { + glog.V(3).Infof("Error deleting Secret: %v", err) + } + } +} diff --git a/pkg/controller/bootstrap/tokencleaner_test.go b/pkg/controller/bootstrap/tokencleaner_test.go new file mode 100644 index 00000000000..f6de895d2bb --- /dev/null +++ b/pkg/controller/bootstrap/tokencleaner_test.go @@ -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 ( + "testing" + "time" + + "github.com/davecgh/go-spew/spew" + + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/pkg/api" + core "k8s.io/client-go/testing" +) + +func init() { + spew.Config.DisableMethods = true +} + +func newTokenCleaner() (*TokenCleaner, *fake.Clientset) { + options := DefaultTokenCleanerOptions() + cl := fake.NewSimpleClientset() + return NewTokenCleaner(cl, options), cl +} + +func TestCleanerNoExpiration(t *testing.T) { + cleaner, cl := newTokenCleaner() + + secret := newTokenSecret("tokenID", "tokenSecret") + cleaner.secrets.Add(secret) + + cleaner.evalSecret(secret) + + expected := []core.Action{} + + verifyActions(t, expected, cl.Actions()) +} + +func TestCleanerExpired(t *testing.T) { + cleaner, cl := newTokenCleaner() + + secret := newTokenSecret("tokenID", "tokenSecret") + addSecretExpiration(secret, timeString(-time.Hour)) + cleaner.secrets.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 := newTokenCleaner() + + secret := newTokenSecret("tokenID", "tokenSecret") + addSecretExpiration(secret, timeString(time.Hour)) + cleaner.secrets.Add(secret) + + cleaner.evalSecret(secret) + + expected := []core.Action{} + + verifyActions(t, expected, cl.Actions()) +}