Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit d69d054

Browse files
marunsoltysh
authored andcommitted
UPSTREAM: <carry>: Ensure service ca is mounted for projected tokens
OpenShift since 3.x has injected the service serving certificate ca (service ca) bundle into service account token secrets. This was intended to ensure that all pods would be able to easily verify connections to endpoints secured with service serving certificates. Since breaking customer workloads is not an option, and there is no way to ensure that customers are not relying on the service ca bundle being mounted at /var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt, it is necessary to continue mounting the service ca bundle in the same location in the bound token projected volumes enabled by the BoundServiceAccountTokenVolume feature (enabled by default in 1.21). A new controller is added to create a configmap per namespace that is annotated for service ca injection. The controller is derived from the controller that creates configmaps for the root ca. The service account admission controller is updated to include a source for the new configmap in the default projected volume definition. UPSTREAM: <carry>: <squash> Add unit testing for service ca configmap publishing This commit should be squashed with: UPSTREAM: <carry>: Ensure service ca is mounted for projected tokens openshift-rebase(v1.24):source=efe0cfeaa21
1 parent 036b11c commit d69d054

File tree

11 files changed

+599
-0
lines changed

11 files changed

+599
-0
lines changed

cmd/kube-controller-manager/app/certificates.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525

2626
"k8s.io/controller-manager/controller"
2727
"k8s.io/klog/v2"
28+
"k8s.io/kubernetes/openshift-kube-controller-manager/servicecacertpublisher"
2829
"k8s.io/kubernetes/pkg/controller/certificates/approver"
2930
"k8s.io/kubernetes/pkg/controller/certificates/cleaner"
3031
"k8s.io/kubernetes/pkg/controller/certificates/rootcacertpublisher"
@@ -191,3 +192,16 @@ func startRootCACertPublisher(ctx context.Context, controllerContext ControllerC
191192
go sac.Run(ctx, 1)
192193
return nil, true, nil
193194
}
195+
196+
func startServiceCACertPublisher(ctx context.Context, controllerContext ControllerContext) (controller.Interface, bool, error) {
197+
sac, err := servicecacertpublisher.NewPublisher(
198+
controllerContext.InformerFactory.Core().V1().ConfigMaps(),
199+
controllerContext.InformerFactory.Core().V1().Namespaces(),
200+
controllerContext.ClientBuilder.ClientOrDie("service-ca-cert-publisher"),
201+
)
202+
if err != nil {
203+
return nil, true, fmt.Errorf("error creating service CA certificate publisher: %v", err)
204+
}
205+
go sac.Run(1, ctx.Done())
206+
return nil, true, nil
207+
}

cmd/kube-controller-manager/app/controllermanager.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,7 @@ func NewControllerInitializers(loopMode ControllerLoopMode) map[string]InitFunc
496496
controllers["pv-protection"] = startPVProtectionController
497497
controllers["ttl-after-finished"] = startTTLAfterFinishedController
498498
controllers["root-ca-cert-publisher"] = startRootCACertPublisher
499+
controllers["service-ca-cert-publisher"] = startServiceCACertPublisher
499500
controllers["ephemeral-volume"] = startEphemeralVolumeController
500501
if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.APIServerIdentity) &&
501502
utilfeature.DefaultFeatureGate.Enabled(genericfeatures.StorageVersionAPI) {
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package servicecacertpublisher
2+
3+
import (
4+
"strconv"
5+
"sync"
6+
"time"
7+
8+
apierrors "k8s.io/apimachinery/pkg/api/errors"
9+
"k8s.io/component-base/metrics"
10+
"k8s.io/component-base/metrics/legacyregistry"
11+
)
12+
13+
// ServiceCACertPublisher - subsystem name used by service_ca_cert_publisher
14+
const ServiceCACertPublisher = "service_ca_cert_publisher"
15+
16+
var (
17+
syncCounter = metrics.NewCounterVec(
18+
&metrics.CounterOpts{
19+
Subsystem: ServiceCACertPublisher,
20+
Name: "sync_total",
21+
Help: "Number of namespace syncs happened in service ca cert publisher.",
22+
StabilityLevel: metrics.ALPHA,
23+
},
24+
[]string{"code"},
25+
)
26+
syncLatency = metrics.NewHistogramVec(
27+
&metrics.HistogramOpts{
28+
Subsystem: ServiceCACertPublisher,
29+
Name: "sync_duration_seconds",
30+
Help: "Number of namespace syncs happened in service ca cert publisher.",
31+
Buckets: metrics.ExponentialBuckets(0.001, 2, 15),
32+
StabilityLevel: metrics.ALPHA,
33+
},
34+
[]string{"code"},
35+
)
36+
)
37+
38+
func recordMetrics(start time.Time, ns string, err error) {
39+
code := "500"
40+
if err == nil {
41+
code = "200"
42+
} else if se, ok := err.(*apierrors.StatusError); ok && se.Status().Code != 0 {
43+
code = strconv.Itoa(int(se.Status().Code))
44+
}
45+
syncLatency.WithLabelValues(code).Observe(time.Since(start).Seconds())
46+
syncCounter.WithLabelValues(code).Inc()
47+
}
48+
49+
var once sync.Once
50+
51+
func registerMetrics() {
52+
once.Do(func() {
53+
legacyregistry.MustRegister(syncCounter)
54+
legacyregistry.MustRegister(syncLatency)
55+
})
56+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package servicecacertpublisher
2+
3+
import (
4+
"errors"
5+
"strings"
6+
"testing"
7+
"time"
8+
9+
corev1 "k8s.io/api/core/v1"
10+
apierrors "k8s.io/apimachinery/pkg/api/errors"
11+
"k8s.io/component-base/metrics/legacyregistry"
12+
"k8s.io/component-base/metrics/testutil"
13+
)
14+
15+
func TestSyncCounter(t *testing.T) {
16+
testCases := []struct {
17+
desc string
18+
err error
19+
metrics []string
20+
want string
21+
}{
22+
{
23+
desc: "nil error",
24+
err: nil,
25+
metrics: []string{
26+
"service_ca_cert_publisher_sync_total",
27+
},
28+
want: `
29+
# HELP service_ca_cert_publisher_sync_total [ALPHA] Number of namespace syncs happened in service ca cert publisher.
30+
# TYPE service_ca_cert_publisher_sync_total counter
31+
service_ca_cert_publisher_sync_total{code="200"} 1
32+
`,
33+
},
34+
{
35+
desc: "kube api error",
36+
err: apierrors.NewNotFound(corev1.Resource("configmap"), "test-configmap"),
37+
metrics: []string{
38+
"service_ca_cert_publisher_sync_total",
39+
},
40+
want: `
41+
# HELP service_ca_cert_publisher_sync_total [ALPHA] Number of namespace syncs happened in service ca cert publisher.
42+
# TYPE service_ca_cert_publisher_sync_total counter
43+
service_ca_cert_publisher_sync_total{code="404"} 1
44+
`,
45+
},
46+
{
47+
desc: "kube api error without code",
48+
err: &apierrors.StatusError{},
49+
metrics: []string{
50+
"service_ca_cert_publisher_sync_total",
51+
},
52+
want: `
53+
# HELP service_ca_cert_publisher_sync_total [ALPHA] Number of namespace syncs happened in service ca cert publisher.
54+
# TYPE service_ca_cert_publisher_sync_total counter
55+
service_ca_cert_publisher_sync_total{code="500"} 1
56+
`,
57+
},
58+
{
59+
desc: "general error",
60+
err: errors.New("test"),
61+
metrics: []string{
62+
"service_ca_cert_publisher_sync_total",
63+
},
64+
want: `
65+
# HELP service_ca_cert_publisher_sync_total [ALPHA] Number of namespace syncs happened in service ca cert publisher.
66+
# TYPE service_ca_cert_publisher_sync_total counter
67+
service_ca_cert_publisher_sync_total{code="500"} 1
68+
`,
69+
},
70+
}
71+
72+
for _, tc := range testCases {
73+
t.Run(tc.desc, func(t *testing.T) {
74+
recordMetrics(time.Now(), "test-ns", tc.err)
75+
defer syncCounter.Reset()
76+
if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(tc.want), tc.metrics...); err != nil {
77+
t.Fatal(err)
78+
}
79+
})
80+
}
81+
}
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
package servicecacertpublisher
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"reflect"
7+
"time"
8+
9+
v1 "k8s.io/api/core/v1"
10+
apierrors "k8s.io/apimachinery/pkg/api/errors"
11+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
12+
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
13+
"k8s.io/apimachinery/pkg/util/wait"
14+
coreinformers "k8s.io/client-go/informers/core/v1"
15+
clientset "k8s.io/client-go/kubernetes"
16+
corelisters "k8s.io/client-go/listers/core/v1"
17+
"k8s.io/client-go/tools/cache"
18+
"k8s.io/client-go/util/workqueue"
19+
"k8s.io/component-base/metrics/prometheus/ratelimiter"
20+
"k8s.io/klog/v2"
21+
)
22+
23+
// ServiceCACertConfigMapName is name of the configmap which stores certificates
24+
// to validate service serving certificates issued by the service ca operator.
25+
const ServiceCACertConfigMapName = "openshift-service-ca.crt"
26+
27+
func init() {
28+
registerMetrics()
29+
}
30+
31+
// NewPublisher construct a new controller which would manage the configmap
32+
// which stores certificates in each namespace. It will make sure certificate
33+
// configmap exists in each namespace.
34+
func NewPublisher(cmInformer coreinformers.ConfigMapInformer, nsInformer coreinformers.NamespaceInformer, cl clientset.Interface) (*Publisher, error) {
35+
e := &Publisher{
36+
client: cl,
37+
queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "service_ca_cert_publisher"),
38+
}
39+
if cl.CoreV1().RESTClient().GetRateLimiter() != nil {
40+
if err := ratelimiter.RegisterMetricAndTrackRateLimiterUsage("service_ca_cert_publisher", cl.CoreV1().RESTClient().GetRateLimiter()); err != nil {
41+
return nil, err
42+
}
43+
}
44+
45+
cmInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
46+
DeleteFunc: e.configMapDeleted,
47+
UpdateFunc: e.configMapUpdated,
48+
})
49+
e.cmLister = cmInformer.Lister()
50+
e.cmListerSynced = cmInformer.Informer().HasSynced
51+
52+
nsInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
53+
AddFunc: e.namespaceAdded,
54+
UpdateFunc: e.namespaceUpdated,
55+
})
56+
e.nsListerSynced = nsInformer.Informer().HasSynced
57+
58+
e.syncHandler = e.syncNamespace
59+
60+
return e, nil
61+
}
62+
63+
// Publisher manages certificate ConfigMap objects inside Namespaces
64+
type Publisher struct {
65+
client clientset.Interface
66+
67+
// To allow injection for testing.
68+
syncHandler func(key string) error
69+
70+
cmLister corelisters.ConfigMapLister
71+
cmListerSynced cache.InformerSynced
72+
73+
nsListerSynced cache.InformerSynced
74+
75+
queue workqueue.RateLimitingInterface
76+
}
77+
78+
// Run starts process
79+
func (c *Publisher) Run(workers int, stopCh <-chan struct{}) {
80+
defer utilruntime.HandleCrash()
81+
defer c.queue.ShutDown()
82+
83+
klog.Infof("Starting service CA certificate configmap publisher")
84+
defer klog.Infof("Shutting down service CA certificate configmap publisher")
85+
86+
if !cache.WaitForNamedCacheSync("crt configmap", stopCh, c.cmListerSynced) {
87+
return
88+
}
89+
90+
for i := 0; i < workers; i++ {
91+
go wait.Until(c.runWorker, time.Second, stopCh)
92+
}
93+
94+
<-stopCh
95+
}
96+
97+
func (c *Publisher) configMapDeleted(obj interface{}) {
98+
cm, err := convertToCM(obj)
99+
if err != nil {
100+
utilruntime.HandleError(err)
101+
return
102+
}
103+
if cm.Name != ServiceCACertConfigMapName {
104+
return
105+
}
106+
c.queue.Add(cm.Namespace)
107+
}
108+
109+
func (c *Publisher) configMapUpdated(_, newObj interface{}) {
110+
cm, err := convertToCM(newObj)
111+
if err != nil {
112+
utilruntime.HandleError(err)
113+
return
114+
}
115+
if cm.Name != ServiceCACertConfigMapName {
116+
return
117+
}
118+
c.queue.Add(cm.Namespace)
119+
}
120+
121+
func (c *Publisher) namespaceAdded(obj interface{}) {
122+
namespace := obj.(*v1.Namespace)
123+
c.queue.Add(namespace.Name)
124+
}
125+
126+
func (c *Publisher) namespaceUpdated(oldObj interface{}, newObj interface{}) {
127+
newNamespace := newObj.(*v1.Namespace)
128+
if newNamespace.Status.Phase != v1.NamespaceActive {
129+
return
130+
}
131+
c.queue.Add(newNamespace.Name)
132+
}
133+
134+
func (c *Publisher) runWorker() {
135+
for c.processNextWorkItem() {
136+
}
137+
}
138+
139+
// processNextWorkItem deals with one key off the queue. It returns false when
140+
// it's time to quit.
141+
func (c *Publisher) processNextWorkItem() bool {
142+
key, quit := c.queue.Get()
143+
if quit {
144+
return false
145+
}
146+
defer c.queue.Done(key)
147+
148+
if err := c.syncHandler(key.(string)); err != nil {
149+
utilruntime.HandleError(fmt.Errorf("syncing %q failed: %v", key, err))
150+
c.queue.AddRateLimited(key)
151+
return true
152+
}
153+
154+
c.queue.Forget(key)
155+
return true
156+
}
157+
158+
func (c *Publisher) syncNamespace(ns string) (err error) {
159+
startTime := time.Now()
160+
defer func() {
161+
recordMetrics(startTime, ns, err)
162+
klog.V(4).Infof("Finished syncing namespace %q (%v)", ns, time.Since(startTime))
163+
}()
164+
165+
annotations := map[string]string{
166+
// This annotation prompts the service ca operator to inject
167+
// the service ca bundle into the configmap.
168+
"service.beta.openshift.io/inject-cabundle": "true",
169+
}
170+
171+
cm, err := c.cmLister.ConfigMaps(ns).Get(ServiceCACertConfigMapName)
172+
switch {
173+
case apierrors.IsNotFound(err):
174+
_, err = c.client.CoreV1().ConfigMaps(ns).Create(context.TODO(), &v1.ConfigMap{
175+
ObjectMeta: metav1.ObjectMeta{
176+
Name: ServiceCACertConfigMapName,
177+
Annotations: annotations,
178+
},
179+
// Create new configmaps with the field referenced by the default
180+
// projected volume. This ensures that pods - including the pod for
181+
// service ca operator - will be able to start during initial
182+
// deployment before the service ca operator has responded to the
183+
// injection annotation.
184+
Data: map[string]string{
185+
"service-ca.crt": "",
186+
},
187+
}, metav1.CreateOptions{})
188+
// don't retry a create if the namespace doesn't exist or is terminating
189+
if apierrors.IsNotFound(err) || apierrors.HasStatusCause(err, v1.NamespaceTerminatingCause) {
190+
return nil
191+
}
192+
return err
193+
case err != nil:
194+
return err
195+
}
196+
197+
if reflect.DeepEqual(cm.Annotations, annotations) {
198+
return nil
199+
}
200+
201+
// copy so we don't modify the cache's instance of the configmap
202+
cm = cm.DeepCopy()
203+
cm.Annotations = annotations
204+
205+
_, err = c.client.CoreV1().ConfigMaps(ns).Update(context.TODO(), cm, metav1.UpdateOptions{})
206+
return err
207+
}
208+
209+
func convertToCM(obj interface{}) (*v1.ConfigMap, error) {
210+
cm, ok := obj.(*v1.ConfigMap)
211+
if !ok {
212+
tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
213+
if !ok {
214+
return nil, fmt.Errorf("couldn't get object from tombstone %#v", obj)
215+
}
216+
cm, ok = tombstone.Obj.(*v1.ConfigMap)
217+
if !ok {
218+
return nil, fmt.Errorf("tombstone contained object that is not a ConfigMap %#v", obj)
219+
}
220+
}
221+
return cm, nil
222+
}

0 commit comments

Comments
 (0)