From 443e4ea0df376d4f10eaab0af7858d78c59b5de7 Mon Sep 17 00:00:00 2001 From: David Eads Date: Thu, 8 Apr 2021 10:23:35 -0400 Subject: [PATCH] include description of what kube-root-ca.crt can be used to verify --- .../rootcacertpublisher/publisher.go | 17 +++- .../rootcacertpublisher/publisher_test.go | 97 +++++++++++++++++++ 2 files changed, 111 insertions(+), 3 deletions(-) diff --git a/pkg/controller/certificates/rootcacertpublisher/publisher.go b/pkg/controller/certificates/rootcacertpublisher/publisher.go index f0411517ef8..b7e3d80533c 100644 --- a/pkg/controller/certificates/rootcacertpublisher/publisher.go +++ b/pkg/controller/certificates/rootcacertpublisher/publisher.go @@ -38,7 +38,12 @@ import ( // RootCACertConfigMapName is name of the configmap which stores certificates // to access api-server -const RootCACertConfigMapName = "kube-root-ca.crt" +const ( + RootCACertConfigMapName = "kube-root-ca.crt" + DescriptionAnnotation = "kubernetes.io/description" + Description = "Contains a CA bundle that can be used to verify the kube-apiserver when using internal endpoints such as the internal service IP or kubernetes.default.svc. " + + "No other usage is guaranteed across distributions of Kubernetes clusters." +) func init() { registerMetrics() @@ -186,7 +191,8 @@ func (c *Publisher) syncNamespace(ns string) (err error) { case apierrors.IsNotFound(err): _, err = c.client.CoreV1().ConfigMaps(ns).Create(context.TODO(), &v1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Name: RootCACertConfigMapName, + Name: RootCACertConfigMapName, + Annotations: map[string]string{DescriptionAnnotation: Description}, }, Data: map[string]string{ "ca.crt": string(c.rootCA), @@ -205,13 +211,18 @@ func (c *Publisher) syncNamespace(ns string) (err error) { "ca.crt": string(c.rootCA), } - if reflect.DeepEqual(cm.Data, data) { + // ensure the data and the one annotation describing usage of this configmap match. + if reflect.DeepEqual(cm.Data, data) && len(cm.Annotations[DescriptionAnnotation]) > 0 { return nil } // copy so we don't modify the cache's instance of the configmap cm = cm.DeepCopy() cm.Data = data + if cm.Annotations == nil { + cm.Annotations = map[string]string{} + } + cm.Annotations[DescriptionAnnotation] = Description _, err = c.client.CoreV1().ConfigMaps(ns).Update(context.TODO(), cm, metav1.UpdateOptions{}) return err diff --git a/pkg/controller/certificates/rootcacertpublisher/publisher_test.go b/pkg/controller/certificates/rootcacertpublisher/publisher_test.go index 594a18d15dd..c416894e595 100644 --- a/pkg/controller/certificates/rootcacertpublisher/publisher_test.go +++ b/pkg/controller/certificates/rootcacertpublisher/publisher_test.go @@ -22,9 +22,13 @@ import ( v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/diff" "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes/fake" + corev1listers "k8s.io/client-go/listers/core/v1" + clienttesting "k8s.io/client-go/testing" + "k8s.io/client-go/tools/cache" "k8s.io/kubernetes/pkg/controller" ) @@ -175,3 +179,96 @@ func defaultCrtConfigMapPtr(rootCA []byte) *v1.ConfigMap { tmp.Namespace = metav1.NamespaceDefault return &tmp } + +func TestConfigMapUpdateNoHotLoop(t *testing.T) { + testcases := map[string]struct { + ExistingConfigMaps []runtime.Object + ExpectActions func(t *testing.T, actions []clienttesting.Action) + }{ + "update-configmap-annotation": { + ExistingConfigMaps: []runtime.Object{ + &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: RootCACertConfigMapName, + }, + Data: map[string]string{"ca.crt": "fake"}, + }, + }, + ExpectActions: func(t *testing.T, actions []clienttesting.Action) { + if len(actions) != 1 { + t.Fatal(actions) + } + if actions[0].GetVerb() != "update" { + t.Fatal(actions) + } + actualObj := actions[0].(clienttesting.UpdateAction).GetObject() + if actualObj.(*v1.ConfigMap).Annotations[DescriptionAnnotation] != Description { + t.Fatal(actions) + } + if !reflect.DeepEqual(actualObj.(*v1.ConfigMap).Data["ca.crt"], "fake") { + t.Fatal(actions) + } + }, + }, + "no-update-configmap-if-annotation-present-and-equal": { + ExistingConfigMaps: []runtime.Object{ + &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: RootCACertConfigMapName, + Annotations: map[string]string{DescriptionAnnotation: Description}, + }, + Data: map[string]string{"ca.crt": "fake"}, + }, + }, + ExpectActions: func(t *testing.T, actions []clienttesting.Action) { + if len(actions) != 0 { + t.Fatal(actions) + } + }, + }, + "no-update-configmap-if-annotation-present-and-different": { + ExistingConfigMaps: []runtime.Object{ + &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: RootCACertConfigMapName, + Annotations: map[string]string{DescriptionAnnotation: "different"}, + }, + Data: map[string]string{"ca.crt": "fake"}, + }, + }, + ExpectActions: func(t *testing.T, actions []clienttesting.Action) { + if len(actions) != 0 { + t.Fatal(actions) + } + }, + }, + } + + for k, tc := range testcases { + t.Run(k, func(t *testing.T) { + client := fake.NewSimpleClientset(tc.ExistingConfigMaps...) + configMapIndexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}) + for _, obj := range tc.ExistingConfigMaps { + configMapIndexer.Add(obj) + } + + // Publisher manages certificate ConfigMap objects inside Namespaces + controller := Publisher{ + client: client, + rootCA: []byte("fake"), + cmLister: corev1listers.NewConfigMapLister(configMapIndexer), + cmListerSynced: func() bool { return true }, + nsListerSynced: func() bool { return true }, + } + + err := controller.syncNamespace("default") + if err != nil { + t.Fatal(err) + } + tc.ExpectActions(t, client.Actions()) + }) + } +}