mirror of
				https://github.com/k3s-io/kubernetes.git
				synced 2025-10-31 05:40:42 +00:00 
			
		
		
		
	kubelet: Support ClusterTrustBundlePEM projections
This commit is contained in:
		| @@ -1002,18 +1002,14 @@ func dropDisabledClusterTrustBundleProjection(podSpec, oldPodSpec *api.PodSpec) | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for _, v := range podSpec.Volumes { | 	for i := range podSpec.Volumes { | ||||||
| 		if v.Projected == nil { | 		if podSpec.Volumes[i].Projected == nil { | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		filteredSources := []api.VolumeProjection{} | 		for j := range podSpec.Volumes[i].Projected.Sources { | ||||||
| 		for _, s := range v.Projected.Sources { | 			podSpec.Volumes[i].Projected.Sources[j].ClusterTrustBundle = nil | ||||||
| 			if s.ClusterTrustBundle == nil { |  | ||||||
| 				filteredSources = append(filteredSources, s) |  | ||||||
| 			} |  | ||||||
| 		} | 		} | ||||||
| 		v.Projected.Sources = filteredSources |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3237,3 +3237,156 @@ func TestMarkPodProposedForResize(t *testing.T) { | |||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func TestDropClusterTrustBundleProjectedVolumes(t *testing.T) { | ||||||
|  | 	testCases := []struct { | ||||||
|  | 		description                         string | ||||||
|  | 		clusterTrustBundleProjectionEnabled bool | ||||||
|  | 		oldPod                              *api.PodSpec | ||||||
|  | 		newPod                              *api.PodSpec | ||||||
|  | 		wantPod                             *api.PodSpec | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			description: "feature gate disabled, cannot add CTB volume to pod", | ||||||
|  | 			oldPod: &api.PodSpec{ | ||||||
|  | 				Volumes: []api.Volume{}, | ||||||
|  | 			}, | ||||||
|  | 			newPod: &api.PodSpec{ | ||||||
|  | 				Volumes: []api.Volume{ | ||||||
|  | 					{ | ||||||
|  | 						Name: "foo", | ||||||
|  | 						VolumeSource: api.VolumeSource{ | ||||||
|  | 							Projected: &api.ProjectedVolumeSource{ | ||||||
|  | 								Sources: []api.VolumeProjection{ | ||||||
|  | 									{ | ||||||
|  | 										ClusterTrustBundle: &api.ClusterTrustBundleProjection{ | ||||||
|  | 											Name: pointer.String("foo"), | ||||||
|  | 										}, | ||||||
|  | 									}, | ||||||
|  | 								}, | ||||||
|  | 							}}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			wantPod: &api.PodSpec{ | ||||||
|  | 				Volumes: []api.Volume{ | ||||||
|  | 					{ | ||||||
|  | 						Name: "foo", | ||||||
|  | 						VolumeSource: api.VolumeSource{ | ||||||
|  | 							Projected: &api.ProjectedVolumeSource{ | ||||||
|  | 								Sources: []api.VolumeProjection{ | ||||||
|  | 									{}, | ||||||
|  | 								}, | ||||||
|  | 							}}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			description: "feature gate disabled, can keep CTB volume on pod", | ||||||
|  | 			oldPod: &api.PodSpec{ | ||||||
|  | 				Volumes: []api.Volume{ | ||||||
|  | 					{ | ||||||
|  | 						Name: "foo", | ||||||
|  | 						VolumeSource: api.VolumeSource{ | ||||||
|  | 							Projected: &api.ProjectedVolumeSource{ | ||||||
|  | 								Sources: []api.VolumeProjection{ | ||||||
|  | 									{ | ||||||
|  | 										ClusterTrustBundle: &api.ClusterTrustBundleProjection{ | ||||||
|  | 											Name: pointer.String("foo"), | ||||||
|  | 										}, | ||||||
|  | 									}, | ||||||
|  | 								}, | ||||||
|  | 							}}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			newPod: &api.PodSpec{ | ||||||
|  | 				Volumes: []api.Volume{ | ||||||
|  | 					{ | ||||||
|  | 						Name: "foo", | ||||||
|  | 						VolumeSource: api.VolumeSource{ | ||||||
|  | 							Projected: &api.ProjectedVolumeSource{ | ||||||
|  | 								Sources: []api.VolumeProjection{ | ||||||
|  | 									{ | ||||||
|  | 										ClusterTrustBundle: &api.ClusterTrustBundleProjection{ | ||||||
|  | 											Name: pointer.String("foo"), | ||||||
|  | 										}, | ||||||
|  | 									}, | ||||||
|  | 								}, | ||||||
|  | 							}}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			wantPod: &api.PodSpec{ | ||||||
|  | 				Volumes: []api.Volume{ | ||||||
|  | 					{ | ||||||
|  | 						Name: "foo", | ||||||
|  | 						VolumeSource: api.VolumeSource{ | ||||||
|  | 							Projected: &api.ProjectedVolumeSource{ | ||||||
|  | 								Sources: []api.VolumeProjection{ | ||||||
|  | 									{ | ||||||
|  | 										ClusterTrustBundle: &api.ClusterTrustBundleProjection{ | ||||||
|  | 											Name: pointer.String("foo"), | ||||||
|  | 										}, | ||||||
|  | 									}, | ||||||
|  | 								}, | ||||||
|  | 							}}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			description:                         "feature gate enabled, can add CTB volume to pod", | ||||||
|  | 			clusterTrustBundleProjectionEnabled: true, | ||||||
|  | 			oldPod: &api.PodSpec{ | ||||||
|  | 				Volumes: []api.Volume{}, | ||||||
|  | 			}, | ||||||
|  | 			newPod: &api.PodSpec{ | ||||||
|  | 				Volumes: []api.Volume{ | ||||||
|  | 					{ | ||||||
|  | 						Name: "foo", | ||||||
|  | 						VolumeSource: api.VolumeSource{ | ||||||
|  | 							Projected: &api.ProjectedVolumeSource{ | ||||||
|  | 								Sources: []api.VolumeProjection{ | ||||||
|  | 									{ | ||||||
|  | 										ClusterTrustBundle: &api.ClusterTrustBundleProjection{ | ||||||
|  | 											Name: pointer.String("foo"), | ||||||
|  | 										}, | ||||||
|  | 									}, | ||||||
|  | 								}, | ||||||
|  | 							}}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			wantPod: &api.PodSpec{ | ||||||
|  | 				Volumes: []api.Volume{ | ||||||
|  | 					{ | ||||||
|  | 						Name: "foo", | ||||||
|  | 						VolumeSource: api.VolumeSource{ | ||||||
|  | 							Projected: &api.ProjectedVolumeSource{ | ||||||
|  | 								Sources: []api.VolumeProjection{ | ||||||
|  | 									{ | ||||||
|  | 										ClusterTrustBundle: &api.ClusterTrustBundleProjection{ | ||||||
|  | 											Name: pointer.String("foo"), | ||||||
|  | 										}, | ||||||
|  | 									}, | ||||||
|  | 								}, | ||||||
|  | 							}}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, tc := range testCases { | ||||||
|  | 		t.Run(tc.description, func(t *testing.T) { | ||||||
|  | 			defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.ClusterTrustBundleProjection, tc.clusterTrustBundleProjectionEnabled)() | ||||||
|  |  | ||||||
|  | 			dropDisabledClusterTrustBundleProjection(tc.newPod, tc.oldPod) | ||||||
|  | 			if diff := cmp.Diff(tc.newPod, tc.wantPod); diff != "" { | ||||||
|  | 				t.Fatalf("Unexpected modification to new pod; diff (-got +want)\n%s", diff) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										261
									
								
								pkg/kubelet/clustertrustbundle/clustertrustbundle_manager.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										261
									
								
								pkg/kubelet/clustertrustbundle/clustertrustbundle_manager.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,261 @@ | |||||||
|  | /* | ||||||
|  | Copyright 2023 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 clustertrustbundle abstracts access to ClusterTrustBundles so that | ||||||
|  | // projected volumes can use them. | ||||||
|  | package clustertrustbundle | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/pem" | ||||||
|  | 	"fmt" | ||||||
|  | 	"math/rand" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	certificatesv1alpha1 "k8s.io/api/certificates/v1alpha1" | ||||||
|  | 	k8serrors "k8s.io/apimachinery/pkg/api/errors" | ||||||
|  | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
|  | 	lrucache "k8s.io/apimachinery/pkg/util/cache" | ||||||
|  | 	"k8s.io/apimachinery/pkg/util/sets" | ||||||
|  | 	certinformersv1alpha1 "k8s.io/client-go/informers/certificates/v1alpha1" | ||||||
|  | 	certlistersv1alpha1 "k8s.io/client-go/listers/certificates/v1alpha1" | ||||||
|  | 	"k8s.io/client-go/tools/cache" | ||||||
|  | 	"k8s.io/klog/v2" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	maxLabelSelectorLength = 100 * 1024 | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // Manager abstracts over the ability to get trust anchors. | ||||||
|  | type Manager interface { | ||||||
|  | 	GetTrustAnchorsByName(name string, allowMissing bool) ([]byte, error) | ||||||
|  | 	GetTrustAnchorsBySigner(signerName string, labelSelector *metav1.LabelSelector, allowMissing bool) ([]byte, error) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // InformerManager is the "real" manager.  It uses informers to track | ||||||
|  | // ClusterTrustBundle objects. | ||||||
|  | type InformerManager struct { | ||||||
|  | 	ctbInformer cache.SharedIndexInformer | ||||||
|  | 	ctbLister   certlistersv1alpha1.ClusterTrustBundleLister | ||||||
|  |  | ||||||
|  | 	normalizationCache *lrucache.LRUExpireCache | ||||||
|  | 	cacheTTL           time.Duration | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var _ Manager = (*InformerManager)(nil) | ||||||
|  |  | ||||||
|  | // NewInformerManager returns an initialized InformerManager. | ||||||
|  | func NewInformerManager(bundles certinformersv1alpha1.ClusterTrustBundleInformer, cacheSize int, cacheTTL time.Duration) (*InformerManager, error) { | ||||||
|  | 	// We need to call Informer() before calling start on the shared informer | ||||||
|  | 	// factory, or the informer won't be registered to be started. | ||||||
|  | 	m := &InformerManager{ | ||||||
|  | 		ctbInformer:        bundles.Informer(), | ||||||
|  | 		ctbLister:          bundles.Lister(), | ||||||
|  | 		normalizationCache: lrucache.NewLRUExpireCache(cacheSize), | ||||||
|  | 		cacheTTL:           cacheTTL, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Have the informer bust cache entries when it sees updates that could | ||||||
|  | 	// apply to them. | ||||||
|  | 	_, err := m.ctbInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ | ||||||
|  | 		AddFunc: func(obj any) { | ||||||
|  | 			ctb, ok := obj.(*certificatesv1alpha1.ClusterTrustBundle) | ||||||
|  | 			if !ok { | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 			klog.InfoS("Dropping all cache entries for signer", "signerName", ctb.Spec.SignerName) | ||||||
|  | 			m.dropCacheFor(ctb) | ||||||
|  | 		}, | ||||||
|  | 		UpdateFunc: func(old, new any) { | ||||||
|  | 			ctb, ok := new.(*certificatesv1alpha1.ClusterTrustBundle) | ||||||
|  | 			if !ok { | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 			klog.InfoS("Dropping cache for ClusterTrustBundle", "signerName", ctb.Spec.SignerName) | ||||||
|  | 			m.dropCacheFor(new.(*certificatesv1alpha1.ClusterTrustBundle)) | ||||||
|  | 		}, | ||||||
|  | 		DeleteFunc: func(obj any) { | ||||||
|  | 			ctb, ok := obj.(*certificatesv1alpha1.ClusterTrustBundle) | ||||||
|  | 			if !ok { | ||||||
|  | 				tombstone, ok := obj.(cache.DeletedFinalStateUnknown) | ||||||
|  | 				if !ok { | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 				ctb, ok = tombstone.Obj.(*certificatesv1alpha1.ClusterTrustBundle) | ||||||
|  | 				if !ok { | ||||||
|  | 					return | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			klog.InfoS("Dropping cache for ClusterTrustBundle", "signerName", ctb.Spec.SignerName) | ||||||
|  | 			m.dropCacheFor(ctb) | ||||||
|  | 		}, | ||||||
|  | 	}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("while registering event handler on informer: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return m, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (m *InformerManager) dropCacheFor(ctb *certificatesv1alpha1.ClusterTrustBundle) { | ||||||
|  | 	if ctb.Spec.SignerName != "" { | ||||||
|  | 		m.normalizationCache.RemoveAll(func(key any) bool { | ||||||
|  | 			return key.(cacheKeyType).signerName == ctb.Spec.SignerName | ||||||
|  | 		}) | ||||||
|  | 	} else { | ||||||
|  | 		m.normalizationCache.RemoveAll(func(key any) bool { | ||||||
|  | 			return key.(cacheKeyType).ctbName == ctb.ObjectMeta.Name | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetTrustAnchorsByName returns normalized and deduplicated trust anchors from | ||||||
|  | // a single named ClusterTrustBundle. | ||||||
|  | func (m *InformerManager) GetTrustAnchorsByName(name string, allowMissing bool) ([]byte, error) { | ||||||
|  | 	if !m.ctbInformer.HasSynced() { | ||||||
|  | 		return nil, fmt.Errorf("ClusterTrustBundle informer has not yet synced") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	cacheKey := cacheKeyType{ctbName: name} | ||||||
|  |  | ||||||
|  | 	if cachedAnchors, ok := m.normalizationCache.Get(cacheKey); ok { | ||||||
|  | 		return cachedAnchors.([]byte), nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ctb, err := m.ctbLister.Get(name) | ||||||
|  | 	if k8serrors.IsNotFound(err) && allowMissing { | ||||||
|  | 		return []byte{}, nil | ||||||
|  | 	} | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("while getting ClusterTrustBundle: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	pemTrustAnchors, err := m.normalizeTrustAnchors([]*certificatesv1alpha1.ClusterTrustBundle{ctb}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("while normalizing trust anchors: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	m.normalizationCache.Add(cacheKey, pemTrustAnchors, m.cacheTTL) | ||||||
|  |  | ||||||
|  | 	return pemTrustAnchors, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetTrustAnchorsBySigner returns normalized and deduplicated trust anchors | ||||||
|  | // from a set of selected ClusterTrustBundles. | ||||||
|  | func (m *InformerManager) GetTrustAnchorsBySigner(signerName string, labelSelector *metav1.LabelSelector, allowMissing bool) ([]byte, error) { | ||||||
|  | 	if !m.ctbInformer.HasSynced() { | ||||||
|  | 		return nil, fmt.Errorf("ClusterTrustBundle informer has not yet synced") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Note that this function treats nil as "match nothing", and non-nil but | ||||||
|  | 	// empty as "match everything". | ||||||
|  | 	selector, err := metav1.LabelSelectorAsSelector(labelSelector) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("while parsing label selector: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	cacheKey := cacheKeyType{signerName: signerName, labelSelector: selector.String()} | ||||||
|  |  | ||||||
|  | 	if lsLen := len(cacheKey.labelSelector); lsLen > maxLabelSelectorLength { | ||||||
|  | 		return nil, fmt.Errorf("label selector length (%d) is larger than %d", lsLen, maxLabelSelectorLength) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if cachedAnchors, ok := m.normalizationCache.Get(cacheKey); ok { | ||||||
|  | 		return cachedAnchors.([]byte), nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	rawCTBList, err := m.ctbLister.List(selector) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("while listing ClusterTrustBundles matching label selector %v: %w", labelSelector, err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ctbList := []*certificatesv1alpha1.ClusterTrustBundle{} | ||||||
|  | 	for _, ctb := range rawCTBList { | ||||||
|  | 		if ctb.Spec.SignerName == signerName { | ||||||
|  | 			ctbList = append(ctbList, ctb) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if len(ctbList) == 0 { | ||||||
|  | 		if allowMissing { | ||||||
|  | 			return []byte{}, nil | ||||||
|  | 		} | ||||||
|  | 		return nil, fmt.Errorf("combination of signerName and labelSelector matched zero ClusterTrustBundles") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	pemTrustAnchors, err := m.normalizeTrustAnchors(ctbList) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("while normalizing trust anchors: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	m.normalizationCache.Add(cacheKey, pemTrustAnchors, m.cacheTTL) | ||||||
|  |  | ||||||
|  | 	return pemTrustAnchors, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (m *InformerManager) normalizeTrustAnchors(ctbList []*certificatesv1alpha1.ClusterTrustBundle) ([]byte, error) { | ||||||
|  | 	// Deduplicate trust anchors from all ClusterTrustBundles. | ||||||
|  | 	trustAnchorSet := sets.Set[string]{} | ||||||
|  | 	for _, ctb := range ctbList { | ||||||
|  | 		rest := []byte(ctb.Spec.TrustBundle) | ||||||
|  | 		var b *pem.Block | ||||||
|  | 		for { | ||||||
|  | 			b, rest = pem.Decode(rest) | ||||||
|  | 			if b == nil { | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 			trustAnchorSet = trustAnchorSet.Insert(string(b.Bytes)) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Give the list a stable ordering that changes each time Kubelet restarts. | ||||||
|  | 	trustAnchorList := sets.List(trustAnchorSet) | ||||||
|  | 	rand.Shuffle(len(trustAnchorList), func(i, j int) { | ||||||
|  | 		trustAnchorList[i], trustAnchorList[j] = trustAnchorList[j], trustAnchorList[i] | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	pemTrustAnchors := []byte{} | ||||||
|  | 	for _, ta := range trustAnchorList { | ||||||
|  | 		b := &pem.Block{ | ||||||
|  | 			Type:  "CERTIFICATE", | ||||||
|  | 			Bytes: []byte(ta), | ||||||
|  | 		} | ||||||
|  | 		pemTrustAnchors = append(pemTrustAnchors, pem.EncodeToMemory(b)...) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return pemTrustAnchors, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type cacheKeyType struct { | ||||||
|  | 	ctbName       string | ||||||
|  | 	signerName    string | ||||||
|  | 	labelSelector string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NoopManager always returns an error, for use in static kubelet mode. | ||||||
|  | type NoopManager struct{} | ||||||
|  |  | ||||||
|  | var _ Manager = (*NoopManager)(nil) | ||||||
|  |  | ||||||
|  | // GetTrustAnchorsByName implements Manager. | ||||||
|  | func (m *NoopManager) GetTrustAnchorsByName(name string, allowMissing bool) ([]byte, error) { | ||||||
|  | 	return nil, fmt.Errorf("ClusterTrustBundle projection is not supported in static kubelet mode") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetTrustAnchorsBySigner implements Manager. | ||||||
|  | func (m *NoopManager) GetTrustAnchorsBySigner(signerName string, labelSelector *metav1.LabelSelector, allowMissing bool) ([]byte, error) { | ||||||
|  | 	return nil, fmt.Errorf("ClusterTrustBundle projection is not supported in static kubelet mode") | ||||||
|  | } | ||||||
| @@ -0,0 +1,474 @@ | |||||||
|  | /* | ||||||
|  | Copyright 2023 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 clustertrustbundle | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"context" | ||||||
|  | 	"crypto/ed25519" | ||||||
|  | 	"crypto/rand" | ||||||
|  | 	"crypto/x509" | ||||||
|  | 	"crypto/x509/pkix" | ||||||
|  | 	"encoding/pem" | ||||||
|  | 	"fmt" | ||||||
|  | 	"math/big" | ||||||
|  | 	"sort" | ||||||
|  | 	"strings" | ||||||
|  | 	"testing" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/google/go-cmp/cmp" | ||||||
|  | 	certificatesv1alpha1 "k8s.io/api/certificates/v1alpha1" | ||||||
|  | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
|  | 	"k8s.io/client-go/informers" | ||||||
|  | 	"k8s.io/client-go/kubernetes/fake" | ||||||
|  | 	"k8s.io/client-go/tools/cache" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestBeforeSynced(t *testing.T) { | ||||||
|  | 	kc := fake.NewSimpleClientset() | ||||||
|  |  | ||||||
|  | 	informerFactory := informers.NewSharedInformerFactoryWithOptions(kc, 0) | ||||||
|  |  | ||||||
|  | 	ctbInformer := informerFactory.Certificates().V1alpha1().ClusterTrustBundles() | ||||||
|  | 	ctbManager, _ := NewInformerManager(ctbInformer, 256, 5*time.Minute) | ||||||
|  |  | ||||||
|  | 	_, err := ctbManager.GetTrustAnchorsByName("foo", false) | ||||||
|  | 	if err == nil { | ||||||
|  | 		t.Fatalf("Got nil error, wanted non-nil") | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestGetTrustAnchorsByName(t *testing.T) { | ||||||
|  | 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | ||||||
|  | 	defer cancel() | ||||||
|  |  | ||||||
|  | 	ctb1 := &certificatesv1alpha1.ClusterTrustBundle{ | ||||||
|  | 		ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 			Name: "ctb1", | ||||||
|  | 		}, | ||||||
|  | 		Spec: certificatesv1alpha1.ClusterTrustBundleSpec{ | ||||||
|  | 			TrustBundle: mustMakeRoot(t, "root1"), | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ctb2 := &certificatesv1alpha1.ClusterTrustBundle{ | ||||||
|  | 		ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 			Name: "ctb2", | ||||||
|  | 		}, | ||||||
|  | 		Spec: certificatesv1alpha1.ClusterTrustBundleSpec{ | ||||||
|  | 			TrustBundle: mustMakeRoot(t, "root2"), | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	kc := fake.NewSimpleClientset(ctb1, ctb2) | ||||||
|  |  | ||||||
|  | 	informerFactory := informers.NewSharedInformerFactoryWithOptions(kc, 0) | ||||||
|  |  | ||||||
|  | 	ctbInformer := informerFactory.Certificates().V1alpha1().ClusterTrustBundles() | ||||||
|  | 	ctbManager, _ := NewInformerManager(ctbInformer, 256, 5*time.Minute) | ||||||
|  |  | ||||||
|  | 	informerFactory.Start(ctx.Done()) | ||||||
|  | 	if !cache.WaitForCacheSync(ctx.Done(), ctbInformer.Informer().HasSynced) { | ||||||
|  | 		t.Fatalf("Timed out waiting for informer to sync") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	gotBundle, err := ctbManager.GetTrustAnchorsByName("ctb1", false) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Error while calling GetTrustAnchorsByName: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if diff := diffBundles(gotBundle, []byte(ctb1.Spec.TrustBundle)); diff != "" { | ||||||
|  | 		t.Fatalf("Got bad bundle; diff (-got +want)\n%s", diff) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	gotBundle, err = ctbManager.GetTrustAnchorsByName("ctb2", false) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Error while calling GetTrustAnchorsByName: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if diff := diffBundles(gotBundle, []byte(ctb2.Spec.TrustBundle)); diff != "" { | ||||||
|  | 		t.Fatalf("Got bad bundle; diff (-got +want)\n%s", diff) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	_, err = ctbManager.GetTrustAnchorsByName("not-found", false) | ||||||
|  | 	if err == nil { // EQUALS nil | ||||||
|  | 		t.Fatalf("While looking up nonexisting ClusterTrustBundle, got nil error, wanted non-nil") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	_, err = ctbManager.GetTrustAnchorsByName("not-found", true) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Unexpected error while calling GetTrustAnchorsByName for nonexistent CTB with allowMissing: %v", err) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestGetTrustAnchorsByNameCaching(t *testing.T) { | ||||||
|  | 	ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) | ||||||
|  | 	defer cancel() | ||||||
|  |  | ||||||
|  | 	ctb1 := &certificatesv1alpha1.ClusterTrustBundle{ | ||||||
|  | 		ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 			Name: "foo", | ||||||
|  | 		}, | ||||||
|  | 		Spec: certificatesv1alpha1.ClusterTrustBundleSpec{ | ||||||
|  | 			TrustBundle: mustMakeRoot(t, "root1"), | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ctb2 := &certificatesv1alpha1.ClusterTrustBundle{ | ||||||
|  | 		ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 			Name: "foo", | ||||||
|  | 		}, | ||||||
|  | 		Spec: certificatesv1alpha1.ClusterTrustBundleSpec{ | ||||||
|  | 			TrustBundle: mustMakeRoot(t, "root2"), | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	kc := fake.NewSimpleClientset(ctb1) | ||||||
|  |  | ||||||
|  | 	informerFactory := informers.NewSharedInformerFactoryWithOptions(kc, 0) | ||||||
|  |  | ||||||
|  | 	ctbInformer := informerFactory.Certificates().V1alpha1().ClusterTrustBundles() | ||||||
|  | 	ctbManager, _ := NewInformerManager(ctbInformer, 256, 5*time.Minute) | ||||||
|  |  | ||||||
|  | 	informerFactory.Start(ctx.Done()) | ||||||
|  | 	if !cache.WaitForCacheSync(ctx.Done(), ctbInformer.Informer().HasSynced) { | ||||||
|  | 		t.Fatalf("Timed out waiting for informer to sync") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	t.Run("foo should yield the first certificate", func(t *testing.T) { | ||||||
|  | 		gotBundle, err := ctbManager.GetTrustAnchorsByName("foo", false) | ||||||
|  | 		if err != nil { | ||||||
|  | 			t.Fatalf("Got error while calling GetTrustAnchorsBySigner: %v", err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		wantBundle := ctb1.Spec.TrustBundle | ||||||
|  |  | ||||||
|  | 		if diff := diffBundles(gotBundle, []byte(wantBundle)); diff != "" { | ||||||
|  | 			t.Fatalf("Bad bundle; diff (-got +want)\n%s", diff) | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	t.Run("foo should still yield the first certificate", func(t *testing.T) { | ||||||
|  | 		gotBundle, err := ctbManager.GetTrustAnchorsByName("foo", false) | ||||||
|  | 		if err != nil { | ||||||
|  | 			t.Fatalf("Got error while calling GetTrustAnchorsBySigner: %v", err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		wantBundle := ctb1.Spec.TrustBundle | ||||||
|  |  | ||||||
|  | 		if diff := diffBundles(gotBundle, []byte(wantBundle)); diff != "" { | ||||||
|  | 			t.Fatalf("Bad bundle; diff (-got +want)\n%s", diff) | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	if err := kc.CertificatesV1alpha1().ClusterTrustBundles().Delete(ctx, ctb1.ObjectMeta.Name, metav1.DeleteOptions{}); err != nil { | ||||||
|  | 		t.Fatalf("Error while deleting the old CTB: %v", err) | ||||||
|  | 	} | ||||||
|  | 	if _, err := kc.CertificatesV1alpha1().ClusterTrustBundles().Create(ctx, ctb2, metav1.CreateOptions{}); err != nil { | ||||||
|  | 		t.Fatalf("Error while adding new CTB: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// We need to sleep long enough for the informer to notice the new | ||||||
|  | 	// ClusterTrustBundle, but much less than the 5 minutes of the cache TTL. | ||||||
|  | 	// This shows us that the informer is properly clearing the cache. | ||||||
|  | 	time.Sleep(5 * time.Second) | ||||||
|  |  | ||||||
|  | 	t.Run("foo should yield the new certificate", func(t *testing.T) { | ||||||
|  | 		gotBundle, err := ctbManager.GetTrustAnchorsByName("foo", false) | ||||||
|  | 		if err != nil { | ||||||
|  | 			t.Fatalf("Got error while calling GetTrustAnchorsBySigner: %v", err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		wantBundle := ctb2.Spec.TrustBundle | ||||||
|  |  | ||||||
|  | 		if diff := diffBundles(gotBundle, []byte(wantBundle)); diff != "" { | ||||||
|  | 			t.Fatalf("Bad bundle; diff (-got +want)\n%s", diff) | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestGetTrustAnchorsBySignerName(t *testing.T) { | ||||||
|  | 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | ||||||
|  | 	defer cancel() | ||||||
|  |  | ||||||
|  | 	ctb1 := mustMakeCTB("signer-a-label-a-1", "foo.bar/a", map[string]string{"label": "a"}, mustMakeRoot(t, "0")) | ||||||
|  | 	ctb2 := mustMakeCTB("signer-a-label-a-2", "foo.bar/a", map[string]string{"label": "a"}, mustMakeRoot(t, "1")) | ||||||
|  | 	ctb2dup := mustMakeCTB("signer-a-label-2-dup", "foo.bar/a", map[string]string{"label": "a"}, ctb2.Spec.TrustBundle) | ||||||
|  | 	ctb3 := mustMakeCTB("signer-a-label-b-1", "foo.bar/a", map[string]string{"label": "b"}, mustMakeRoot(t, "2")) | ||||||
|  | 	ctb4 := mustMakeCTB("signer-b-label-a-1", "foo.bar/b", map[string]string{"label": "a"}, mustMakeRoot(t, "3")) | ||||||
|  |  | ||||||
|  | 	kc := fake.NewSimpleClientset(ctb1, ctb2, ctb2dup, ctb3, ctb4) | ||||||
|  |  | ||||||
|  | 	informerFactory := informers.NewSharedInformerFactoryWithOptions(kc, 0) | ||||||
|  |  | ||||||
|  | 	ctbInformer := informerFactory.Certificates().V1alpha1().ClusterTrustBundles() | ||||||
|  | 	ctbManager, _ := NewInformerManager(ctbInformer, 256, 5*time.Minute) | ||||||
|  |  | ||||||
|  | 	informerFactory.Start(ctx.Done()) | ||||||
|  | 	if !cache.WaitForCacheSync(ctx.Done(), ctbInformer.Informer().HasSynced) { | ||||||
|  | 		t.Fatalf("Timed out waiting for informer to sync") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	t.Run("big labelselector should cause error", func(t *testing.T) { | ||||||
|  | 		longString := strings.Builder{} | ||||||
|  | 		for i := 0; i < 63; i++ { | ||||||
|  | 			longString.WriteString("v") | ||||||
|  | 		} | ||||||
|  | 		matchLabels := map[string]string{} | ||||||
|  | 		for i := 0; i < 100*1024/63+1; i++ { | ||||||
|  | 			matchLabels[fmt.Sprintf("key-%d", i)] = longString.String() | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		_, err := ctbManager.GetTrustAnchorsBySigner("foo.bar/a", &metav1.LabelSelector{MatchLabels: matchLabels}, false) | ||||||
|  | 		if err == nil || !strings.Contains(err.Error(), "label selector length") { | ||||||
|  | 			t.Fatalf("Bad error, got %v, wanted it to contain \"label selector length\"", err) | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	t.Run("signer-a label-a should yield two sorted certificates", func(t *testing.T) { | ||||||
|  | 		gotBundle, err := ctbManager.GetTrustAnchorsBySigner("foo.bar/a", &metav1.LabelSelector{MatchLabels: map[string]string{"label": "a"}}, false) | ||||||
|  | 		if err != nil { | ||||||
|  | 			t.Fatalf("Got error while calling GetTrustAnchorsBySigner: %v", err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		wantBundle := ctb1.Spec.TrustBundle + ctb2.Spec.TrustBundle | ||||||
|  |  | ||||||
|  | 		if diff := diffBundles(gotBundle, []byte(wantBundle)); diff != "" { | ||||||
|  | 			t.Fatalf("Bad bundle; diff (-got +want)\n%s", diff) | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	t.Run("signer-a with nil selector should yield zero certificates", func(t *testing.T) { | ||||||
|  | 		gotBundle, err := ctbManager.GetTrustAnchorsBySigner("foo.bar/a", nil, true) | ||||||
|  | 		if err != nil { | ||||||
|  | 			t.Fatalf("Got error while calling GetTrustAnchorsBySigner: %v", err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		wantBundle := "" | ||||||
|  |  | ||||||
|  | 		if diff := diffBundles(gotBundle, []byte(wantBundle)); diff != "" { | ||||||
|  | 			t.Fatalf("Bad bundle; diff (-got +want)\n%s", diff) | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	t.Run("signer-b with empty selector should yield one certificates", func(t *testing.T) { | ||||||
|  | 		gotBundle, err := ctbManager.GetTrustAnchorsBySigner("foo.bar/b", &metav1.LabelSelector{}, false) | ||||||
|  | 		if err != nil { | ||||||
|  | 			t.Fatalf("Got error while calling GetTrustAnchorsBySigner: %v", err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if diff := diffBundles(gotBundle, []byte(ctb4.Spec.TrustBundle)); diff != "" { | ||||||
|  | 			t.Fatalf("Bad bundle; diff (-got +want)\n%s", diff) | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	t.Run("signer-a label-b should yield one certificate", func(t *testing.T) { | ||||||
|  | 		gotBundle, err := ctbManager.GetTrustAnchorsBySigner("foo.bar/a", &metav1.LabelSelector{MatchLabels: map[string]string{"label": "b"}}, false) | ||||||
|  | 		if err != nil { | ||||||
|  | 			t.Fatalf("Got error while calling GetTrustAnchorsBySigner: %v", err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if diff := diffBundles(gotBundle, []byte(ctb3.Spec.TrustBundle)); diff != "" { | ||||||
|  | 			t.Fatalf("Bad bundle; diff (-got +want)\n%s", diff) | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	t.Run("signer-b label-a should yield one certificate", func(t *testing.T) { | ||||||
|  | 		gotBundle, err := ctbManager.GetTrustAnchorsBySigner("foo.bar/b", &metav1.LabelSelector{MatchLabels: map[string]string{"label": "a"}}, false) | ||||||
|  | 		if err != nil { | ||||||
|  | 			t.Fatalf("Got error while calling GetTrustAnchorsBySigner: %v", err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if diff := diffBundles(gotBundle, []byte(ctb4.Spec.TrustBundle)); diff != "" { | ||||||
|  | 			t.Fatalf("Bad bundle; diff (-got +want)\n%s", diff) | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	t.Run("signer-b label-b allowMissing=true should yield zero certificates", func(t *testing.T) { | ||||||
|  | 		gotBundle, err := ctbManager.GetTrustAnchorsBySigner("foo.bar/b", &metav1.LabelSelector{MatchLabels: map[string]string{"label": "b"}}, true) | ||||||
|  | 		if err != nil { | ||||||
|  | 			t.Fatalf("Got error while calling GetTrustAnchorsBySigner: %v", err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if diff := diffBundles(gotBundle, []byte{}); diff != "" { | ||||||
|  | 			t.Fatalf("Bad bundle; diff (-got +want)\n%s", diff) | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	t.Run("signer-b label-b allowMissing=false should yield zero certificates (error)", func(t *testing.T) { | ||||||
|  | 		_, err := ctbManager.GetTrustAnchorsBySigner("foo.bar/b", &metav1.LabelSelector{MatchLabels: map[string]string{"label": "b"}}, false) | ||||||
|  | 		if err == nil { // EQUALS nil | ||||||
|  | 			t.Fatalf("Got nil error while calling GetTrustAnchorsBySigner, wanted non-nil") | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestGetTrustAnchorsBySignerNameCaching(t *testing.T) { | ||||||
|  | 	ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) | ||||||
|  | 	defer cancel() | ||||||
|  |  | ||||||
|  | 	ctb1 := mustMakeCTB("signer-a-label-a-1", "foo.bar/a", map[string]string{"label": "a"}, mustMakeRoot(t, "0")) | ||||||
|  | 	ctb2 := mustMakeCTB("signer-a-label-a-2", "foo.bar/a", map[string]string{"label": "a"}, mustMakeRoot(t, "1")) | ||||||
|  |  | ||||||
|  | 	kc := fake.NewSimpleClientset(ctb1) | ||||||
|  |  | ||||||
|  | 	informerFactory := informers.NewSharedInformerFactoryWithOptions(kc, 0) | ||||||
|  |  | ||||||
|  | 	ctbInformer := informerFactory.Certificates().V1alpha1().ClusterTrustBundles() | ||||||
|  | 	ctbManager, _ := NewInformerManager(ctbInformer, 256, 5*time.Minute) | ||||||
|  |  | ||||||
|  | 	informerFactory.Start(ctx.Done()) | ||||||
|  | 	if !cache.WaitForCacheSync(ctx.Done(), ctbInformer.Informer().HasSynced) { | ||||||
|  | 		t.Fatalf("Timed out waiting for informer to sync") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	t.Run("signer-a label-a should yield one certificate", func(t *testing.T) { | ||||||
|  | 		gotBundle, err := ctbManager.GetTrustAnchorsBySigner("foo.bar/a", &metav1.LabelSelector{MatchLabels: map[string]string{"label": "a"}}, false) | ||||||
|  | 		if err != nil { | ||||||
|  | 			t.Fatalf("Got error while calling GetTrustAnchorsBySigner: %v", err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		wantBundle := ctb1.Spec.TrustBundle | ||||||
|  |  | ||||||
|  | 		if diff := diffBundles(gotBundle, []byte(wantBundle)); diff != "" { | ||||||
|  | 			t.Fatalf("Bad bundle; diff (-got +want)\n%s", diff) | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	t.Run("signer-a label-a should yield the same result when called again", func(t *testing.T) { | ||||||
|  | 		gotBundle, err := ctbManager.GetTrustAnchorsBySigner("foo.bar/a", &metav1.LabelSelector{MatchLabels: map[string]string{"label": "a"}}, false) | ||||||
|  | 		if err != nil { | ||||||
|  | 			t.Fatalf("Got error while calling GetTrustAnchorsBySigner: %v", err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		wantBundle := ctb1.Spec.TrustBundle | ||||||
|  |  | ||||||
|  | 		if diff := diffBundles(gotBundle, []byte(wantBundle)); diff != "" { | ||||||
|  | 			t.Fatalf("Bad bundle; diff (-got +want)\n%s", diff) | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	if err := kc.CertificatesV1alpha1().ClusterTrustBundles().Delete(ctx, ctb1.ObjectMeta.Name, metav1.DeleteOptions{}); err != nil { | ||||||
|  | 		t.Fatalf("Error while deleting the old CTB: %v", err) | ||||||
|  | 	} | ||||||
|  | 	if _, err := kc.CertificatesV1alpha1().ClusterTrustBundles().Create(ctx, ctb2, metav1.CreateOptions{}); err != nil { | ||||||
|  | 		t.Fatalf("Error while adding new CTB: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// We need to sleep long enough for the informer to notice the new | ||||||
|  | 	// ClusterTrustBundle, but much less than the 5 minutes of the cache TTL. | ||||||
|  | 	// This shows us that the informer is properly clearing the cache. | ||||||
|  | 	time.Sleep(5 * time.Second) | ||||||
|  |  | ||||||
|  | 	t.Run("signer-a label-a should return the new certificate", func(t *testing.T) { | ||||||
|  | 		gotBundle, err := ctbManager.GetTrustAnchorsBySigner("foo.bar/a", &metav1.LabelSelector{MatchLabels: map[string]string{"label": "a"}}, false) | ||||||
|  | 		if err != nil { | ||||||
|  | 			t.Fatalf("Got error while calling GetTrustAnchorsBySigner: %v", err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		wantBundle := ctb2.Spec.TrustBundle | ||||||
|  |  | ||||||
|  | 		if diff := diffBundles(gotBundle, []byte(wantBundle)); diff != "" { | ||||||
|  | 			t.Fatalf("Bad bundle; diff (-got +want)\n%s", diff) | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func mustMakeRoot(t *testing.T, cn string) string { | ||||||
|  | 	pub, priv, err := ed25519.GenerateKey(rand.Reader) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Error while generating key: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	template := &x509.Certificate{ | ||||||
|  | 		SerialNumber: big.NewInt(0), | ||||||
|  | 		Subject: pkix.Name{ | ||||||
|  | 			CommonName: cn, | ||||||
|  | 		}, | ||||||
|  | 		IsCA:                  true, | ||||||
|  | 		BasicConstraintsValid: true, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	cert, err := x509.CreateCertificate(rand.Reader, template, template, pub, priv) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Error while making certificate: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return string(pem.EncodeToMemory(&pem.Block{ | ||||||
|  | 		Type:    "CERTIFICATE", | ||||||
|  | 		Headers: nil, | ||||||
|  | 		Bytes:   cert, | ||||||
|  | 	})) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func mustMakeCTB(name, signerName string, labels map[string]string, bundle string) *certificatesv1alpha1.ClusterTrustBundle { | ||||||
|  | 	return &certificatesv1alpha1.ClusterTrustBundle{ | ||||||
|  | 		ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 			Name:   name, | ||||||
|  | 			Labels: labels, | ||||||
|  | 		}, | ||||||
|  | 		Spec: certificatesv1alpha1.ClusterTrustBundleSpec{ | ||||||
|  | 			SignerName:  signerName, | ||||||
|  | 			TrustBundle: bundle, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func diffBundles(a, b []byte) string { | ||||||
|  | 	var block *pem.Block | ||||||
|  |  | ||||||
|  | 	aBlocks := []*pem.Block{} | ||||||
|  | 	for { | ||||||
|  | 		block, a = pem.Decode(a) | ||||||
|  | 		if block == nil { | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 		aBlocks = append(aBlocks, block) | ||||||
|  | 	} | ||||||
|  | 	sort.Slice(aBlocks, func(i, j int) bool { | ||||||
|  | 		if aBlocks[i].Type < aBlocks[j].Type { | ||||||
|  | 			return true | ||||||
|  | 		} else if aBlocks[i].Type == aBlocks[j].Type { | ||||||
|  | 			comp := bytes.Compare(aBlocks[i].Bytes, aBlocks[j].Bytes) | ||||||
|  | 			return comp <= 0 | ||||||
|  | 		} else { | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	bBlocks := []*pem.Block{} | ||||||
|  | 	for { | ||||||
|  | 		block, b = pem.Decode(b) | ||||||
|  | 		if block == nil { | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 		bBlocks = append(bBlocks, block) | ||||||
|  | 	} | ||||||
|  | 	sort.Slice(bBlocks, func(i, j int) bool { | ||||||
|  | 		if bBlocks[i].Type < bBlocks[j].Type { | ||||||
|  | 			return true | ||||||
|  | 		} else if bBlocks[i].Type == bBlocks[j].Type { | ||||||
|  | 			comp := bytes.Compare(bBlocks[i].Bytes, bBlocks[j].Bytes) | ||||||
|  | 			return comp <= 0 | ||||||
|  | 		} else { | ||||||
|  | 			return false | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	return cmp.Diff(aBlocks, bBlocks) | ||||||
|  | } | ||||||
| @@ -19,6 +19,7 @@ package config | |||||||
| import ( | import ( | ||||||
| 	"crypto/md5" | 	"crypto/md5" | ||||||
| 	"encoding/hex" | 	"encoding/hex" | ||||||
|  | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| @@ -102,6 +103,9 @@ func applyDefaults(pod *api.Pod, source string, isFile bool, nodeName types.Node | |||||||
|  |  | ||||||
| type defaultFunc func(pod *api.Pod) error | type defaultFunc func(pod *api.Pod) error | ||||||
|  |  | ||||||
|  | // A static pod tried to use a ClusterTrustBundle projected volume source. | ||||||
|  | var ErrStaticPodTriedToUseClusterTrustBundle = errors.New("static pods may not use ClusterTrustBundle projected volume sources") | ||||||
|  |  | ||||||
| // tryDecodeSinglePod takes data and tries to extract valid Pod config information from it. | // tryDecodeSinglePod takes data and tries to extract valid Pod config information from it. | ||||||
| func tryDecodeSinglePod(data []byte, defaultFn defaultFunc) (parsed bool, pod *v1.Pod, err error) { | func tryDecodeSinglePod(data []byte, defaultFn defaultFunc) (parsed bool, pod *v1.Pod, err error) { | ||||||
| 	// JSON is valid YAML, so this should work for everything. | 	// JSON is valid YAML, so this should work for everything. | ||||||
| @@ -136,6 +140,19 @@ func tryDecodeSinglePod(data []byte, defaultFn defaultFunc) (parsed bool, pod *v | |||||||
| 		klog.ErrorS(err, "Pod failed to convert to v1", "pod", klog.KObj(newPod)) | 		klog.ErrorS(err, "Pod failed to convert to v1", "pod", klog.KObj(newPod)) | ||||||
| 		return true, nil, err | 		return true, nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	for _, v := range v1Pod.Spec.Volumes { | ||||||
|  | 		if v.Projected == nil { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		for _, s := range v.Projected.Sources { | ||||||
|  | 			if s.ClusterTrustBundle != nil { | ||||||
|  | 				return true, nil, ErrStaticPodTriedToUseClusterTrustBundle | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	return true, v1Pod, nil | 	return true, v1Pod, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -17,6 +17,7 @@ limitations under the License. | |||||||
| package config | package config | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
| 	"reflect" | 	"reflect" | ||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
| @@ -31,6 +32,7 @@ import ( | |||||||
| 	"k8s.io/kubernetes/pkg/apis/core" | 	"k8s.io/kubernetes/pkg/apis/core" | ||||||
| 	"k8s.io/kubernetes/pkg/apis/core/validation" | 	"k8s.io/kubernetes/pkg/apis/core/validation" | ||||||
| 	"k8s.io/kubernetes/pkg/securitycontext" | 	"k8s.io/kubernetes/pkg/securitycontext" | ||||||
|  | 	"k8s.io/utils/ptr" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func noDefault(*core.Pod) error { return nil } | func noDefault(*core.Pod) error { return nil } | ||||||
| @@ -107,6 +109,76 @@ func TestDecodeSinglePod(t *testing.T) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func TestDecodeSinglePodRejectsClusterTrustBundleVolumes(t *testing.T) { | ||||||
|  | 	grace := int64(30) | ||||||
|  | 	enableServiceLinks := v1.DefaultEnableServiceLinks | ||||||
|  | 	pod := &v1.Pod{ | ||||||
|  | 		TypeMeta: metav1.TypeMeta{ | ||||||
|  | 			APIVersion: "", | ||||||
|  | 		}, | ||||||
|  | 		ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 			Name:      "test", | ||||||
|  | 			UID:       "12345", | ||||||
|  | 			Namespace: "mynamespace", | ||||||
|  | 		}, | ||||||
|  | 		Spec: v1.PodSpec{ | ||||||
|  | 			RestartPolicy:                 v1.RestartPolicyAlways, | ||||||
|  | 			DNSPolicy:                     v1.DNSClusterFirst, | ||||||
|  | 			TerminationGracePeriodSeconds: &grace, | ||||||
|  | 			Containers: []v1.Container{{ | ||||||
|  | 				Name:                     "image", | ||||||
|  | 				Image:                    "test/image", | ||||||
|  | 				ImagePullPolicy:          "IfNotPresent", | ||||||
|  | 				TerminationMessagePath:   "/dev/termination-log", | ||||||
|  | 				TerminationMessagePolicy: v1.TerminationMessageReadFile, | ||||||
|  | 				SecurityContext:          securitycontext.ValidSecurityContextWithContainerDefaults(), | ||||||
|  | 				VolumeMounts: []v1.VolumeMount{ | ||||||
|  | 					{ | ||||||
|  | 						Name:      "ctb-volume", | ||||||
|  | 						MountPath: "/var/run/ctb-volume", | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}}, | ||||||
|  | 			Volumes: []v1.Volume{ | ||||||
|  | 				{ | ||||||
|  | 					Name: "ctb-volume", | ||||||
|  | 					VolumeSource: v1.VolumeSource{ | ||||||
|  | 						Projected: &v1.ProjectedVolumeSource{ | ||||||
|  | 							Sources: []v1.VolumeProjection{ | ||||||
|  | 								{ | ||||||
|  | 									ClusterTrustBundle: &v1.ClusterTrustBundleProjection{ | ||||||
|  | 										Name: ptr.To("my-ctb"), | ||||||
|  | 										Path: "ctb-file", | ||||||
|  | 									}, | ||||||
|  | 								}, | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			SecurityContext:    &v1.PodSecurityContext{}, | ||||||
|  | 			SchedulerName:      v1.DefaultSchedulerName, | ||||||
|  | 			EnableServiceLinks: &enableServiceLinks, | ||||||
|  | 		}, | ||||||
|  | 		Status: v1.PodStatus{ | ||||||
|  | 			PodIP: "1.2.3.4", | ||||||
|  | 			PodIPs: []v1.PodIP{ | ||||||
|  | 				{ | ||||||
|  | 					IP: "1.2.3.4", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	json, err := runtime.Encode(clientscheme.Codecs.LegacyCodec(v1.SchemeGroupVersion), pod) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Errorf("unexpected error: %v", err) | ||||||
|  | 	} | ||||||
|  | 	_, _, err = tryDecodeSinglePod(json, noDefault) | ||||||
|  | 	if !errors.Is(err, ErrStaticPodTriedToUseClusterTrustBundle) { | ||||||
|  | 		t.Errorf("Got error %q, want %q", err, ErrStaticPodTriedToUseClusterTrustBundle) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| func TestDecodePodList(t *testing.T) { | func TestDecodePodList(t *testing.T) { | ||||||
| 	grace := int64(30) | 	grace := int64(30) | ||||||
| 	enableServiceLinks := v1.DefaultEnableServiceLinks | 	enableServiceLinks := v1.DefaultEnableServiceLinks | ||||||
|   | |||||||
| @@ -75,6 +75,7 @@ import ( | |||||||
| 	"k8s.io/kubernetes/pkg/kubelet/cadvisor" | 	"k8s.io/kubernetes/pkg/kubelet/cadvisor" | ||||||
| 	kubeletcertificate "k8s.io/kubernetes/pkg/kubelet/certificate" | 	kubeletcertificate "k8s.io/kubernetes/pkg/kubelet/certificate" | ||||||
| 	"k8s.io/kubernetes/pkg/kubelet/cloudresource" | 	"k8s.io/kubernetes/pkg/kubelet/cloudresource" | ||||||
|  | 	"k8s.io/kubernetes/pkg/kubelet/clustertrustbundle" | ||||||
| 	"k8s.io/kubernetes/pkg/kubelet/cm" | 	"k8s.io/kubernetes/pkg/kubelet/cm" | ||||||
| 	draplugin "k8s.io/kubernetes/pkg/kubelet/cm/dra/plugin" | 	draplugin "k8s.io/kubernetes/pkg/kubelet/cm/dra/plugin" | ||||||
| 	"k8s.io/kubernetes/pkg/kubelet/config" | 	"k8s.io/kubernetes/pkg/kubelet/config" | ||||||
| @@ -451,7 +452,7 @@ func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration, | |||||||
| 	var serviceLister corelisters.ServiceLister | 	var serviceLister corelisters.ServiceLister | ||||||
| 	var serviceHasSynced cache.InformerSynced | 	var serviceHasSynced cache.InformerSynced | ||||||
| 	if kubeDeps.KubeClient != nil { | 	if kubeDeps.KubeClient != nil { | ||||||
| 		kubeInformers := informers.NewSharedInformerFactory(kubeDeps.KubeClient, 0) | 		kubeInformers := informers.NewSharedInformerFactoryWithOptions(kubeDeps.KubeClient, 0) | ||||||
| 		serviceLister = kubeInformers.Core().V1().Services().Lister() | 		serviceLister = kubeInformers.Core().V1().Services().Lister() | ||||||
| 		serviceHasSynced = kubeInformers.Core().V1().Services().Informer().HasSynced | 		serviceHasSynced = kubeInformers.Core().V1().Services().Informer().HasSynced | ||||||
| 		kubeInformers.Start(wait.NeverStop) | 		kubeInformers.Start(wait.NeverStop) | ||||||
| @@ -793,11 +794,26 @@ func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration, | |||||||
|  |  | ||||||
| 	tokenManager := token.NewManager(kubeDeps.KubeClient) | 	tokenManager := token.NewManager(kubeDeps.KubeClient) | ||||||
|  |  | ||||||
|  | 	var clusterTrustBundleManager clustertrustbundle.Manager | ||||||
|  | 	if kubeDeps.KubeClient != nil && utilfeature.DefaultFeatureGate.Enabled(features.ClusterTrustBundleProjection) { | ||||||
|  | 		kubeInformers := informers.NewSharedInformerFactoryWithOptions(kubeDeps.KubeClient, 0) | ||||||
|  | 		clusterTrustBundleManager, err = clustertrustbundle.NewInformerManager(kubeInformers.Certificates().V1alpha1().ClusterTrustBundles(), 2*int(kubeCfg.MaxPods), 5*time.Minute) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, fmt.Errorf("while starting informer-based ClusterTrustBundle manager: %w", err) | ||||||
|  | 		} | ||||||
|  | 		kubeInformers.Start(wait.NeverStop) | ||||||
|  | 		klog.InfoS("Started ClusterTrustBundle informer") | ||||||
|  | 	} else { | ||||||
|  | 		// In static kubelet mode, use a no-op manager. | ||||||
|  | 		clusterTrustBundleManager = &clustertrustbundle.NoopManager{} | ||||||
|  | 		klog.InfoS("Not starting ClusterTrustBundle informer because we are in static kubelet mode") | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// NewInitializedVolumePluginMgr initializes some storageErrors on the Kubelet runtimeState (in csi_plugin.go init) | 	// NewInitializedVolumePluginMgr initializes some storageErrors on the Kubelet runtimeState (in csi_plugin.go init) | ||||||
| 	// which affects node ready status. This function must be called before Kubelet is initialized so that the Node | 	// which affects node ready status. This function must be called before Kubelet is initialized so that the Node | ||||||
| 	// ReadyState is accurate with the storage state. | 	// ReadyState is accurate with the storage state. | ||||||
| 	klet.volumePluginMgr, err = | 	klet.volumePluginMgr, err = | ||||||
| 		NewInitializedVolumePluginMgr(klet, secretManager, configMapManager, tokenManager, kubeDeps.VolumePlugins, kubeDeps.DynamicPluginProber) | 		NewInitializedVolumePluginMgr(klet, secretManager, configMapManager, tokenManager, clusterTrustBundleManager, kubeDeps.VolumePlugins, kubeDeps.DynamicPluginProber) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -59,6 +59,7 @@ import ( | |||||||
| 	"k8s.io/kubernetes/pkg/features" | 	"k8s.io/kubernetes/pkg/features" | ||||||
| 	kubeletconfiginternal "k8s.io/kubernetes/pkg/kubelet/apis/config" | 	kubeletconfiginternal "k8s.io/kubernetes/pkg/kubelet/apis/config" | ||||||
| 	cadvisortest "k8s.io/kubernetes/pkg/kubelet/cadvisor/testing" | 	cadvisortest "k8s.io/kubernetes/pkg/kubelet/cadvisor/testing" | ||||||
|  | 	"k8s.io/kubernetes/pkg/kubelet/clustertrustbundle" | ||||||
| 	"k8s.io/kubernetes/pkg/kubelet/cm" | 	"k8s.io/kubernetes/pkg/kubelet/cm" | ||||||
| 	"k8s.io/kubernetes/pkg/kubelet/config" | 	"k8s.io/kubernetes/pkg/kubelet/config" | ||||||
| 	"k8s.io/kubernetes/pkg/kubelet/configmap" | 	"k8s.io/kubernetes/pkg/kubelet/configmap" | ||||||
| @@ -379,7 +380,7 @@ func newTestKubeletWithImageList( | |||||||
|  |  | ||||||
| 	var prober volume.DynamicPluginProber // TODO (#51147) inject mock | 	var prober volume.DynamicPluginProber // TODO (#51147) inject mock | ||||||
| 	kubelet.volumePluginMgr, err = | 	kubelet.volumePluginMgr, err = | ||||||
| 		NewInitializedVolumePluginMgr(kubelet, kubelet.secretManager, kubelet.configMapManager, token.NewManager(kubelet.kubeClient), allPlugins, prober) | 		NewInitializedVolumePluginMgr(kubelet, kubelet.secretManager, kubelet.configMapManager, token.NewManager(kubelet.kubeClient), &clustertrustbundle.NoopManager{}, allPlugins, prober) | ||||||
| 	require.NoError(t, err, "Failed to initialize VolumePluginMgr") | 	require.NoError(t, err, "Failed to initialize VolumePluginMgr") | ||||||
|  |  | ||||||
| 	kubelet.volumeManager = kubeletvolume.NewVolumeManager( | 	kubelet.volumeManager = kubeletvolume.NewVolumeManager( | ||||||
|   | |||||||
| @@ -35,6 +35,7 @@ import ( | |||||||
| 	"k8s.io/client-go/tools/record" | 	"k8s.io/client-go/tools/record" | ||||||
| 	utiltesting "k8s.io/client-go/util/testing" | 	utiltesting "k8s.io/client-go/util/testing" | ||||||
| 	cadvisortest "k8s.io/kubernetes/pkg/kubelet/cadvisor/testing" | 	cadvisortest "k8s.io/kubernetes/pkg/kubelet/cadvisor/testing" | ||||||
|  | 	"k8s.io/kubernetes/pkg/kubelet/clustertrustbundle" | ||||||
| 	"k8s.io/kubernetes/pkg/kubelet/cm" | 	"k8s.io/kubernetes/pkg/kubelet/cm" | ||||||
| 	"k8s.io/kubernetes/pkg/kubelet/configmap" | 	"k8s.io/kubernetes/pkg/kubelet/configmap" | ||||||
| 	kubecontainer "k8s.io/kubernetes/pkg/kubelet/container" | 	kubecontainer "k8s.io/kubernetes/pkg/kubelet/container" | ||||||
| @@ -72,6 +73,7 @@ func TestRunOnce(t *testing.T) { | |||||||
| 	}, nil).AnyTimes() | 	}, nil).AnyTimes() | ||||||
| 	fakeSecretManager := secret.NewFakeManager() | 	fakeSecretManager := secret.NewFakeManager() | ||||||
| 	fakeConfigMapManager := configmap.NewFakeManager() | 	fakeConfigMapManager := configmap.NewFakeManager() | ||||||
|  | 	clusterTrustBundleManager := &clustertrustbundle.NoopManager{} | ||||||
| 	podManager := kubepod.NewBasicPodManager() | 	podManager := kubepod.NewBasicPodManager() | ||||||
| 	fakeRuntime := &containertest.FakeRuntime{} | 	fakeRuntime := &containertest.FakeRuntime{} | ||||||
| 	podStartupLatencyTracker := kubeletutil.NewPodStartupLatencyTracker() | 	podStartupLatencyTracker := kubeletutil.NewPodStartupLatencyTracker() | ||||||
| @@ -103,7 +105,7 @@ func TestRunOnce(t *testing.T) { | |||||||
|  |  | ||||||
| 	plug := &volumetest.FakeVolumePlugin{PluginName: "fake", Host: nil} | 	plug := &volumetest.FakeVolumePlugin{PluginName: "fake", Host: nil} | ||||||
| 	kb.volumePluginMgr, err = | 	kb.volumePluginMgr, err = | ||||||
| 		NewInitializedVolumePluginMgr(kb, fakeSecretManager, fakeConfigMapManager, nil, []volume.VolumePlugin{plug}, nil /* prober */) | 		NewInitializedVolumePluginMgr(kb, fakeSecretManager, fakeConfigMapManager, nil, clusterTrustBundleManager, []volume.VolumePlugin{plug}, nil /* prober */) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("failed to initialize VolumePluginMgr: %v", err) | 		t.Fatalf("failed to initialize VolumePluginMgr: %v", err) | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -27,6 +27,7 @@ import ( | |||||||
|  |  | ||||||
| 	authenticationv1 "k8s.io/api/authentication/v1" | 	authenticationv1 "k8s.io/api/authentication/v1" | ||||||
| 	v1 "k8s.io/api/core/v1" | 	v1 "k8s.io/api/core/v1" | ||||||
|  | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
| 	"k8s.io/apimachinery/pkg/types" | 	"k8s.io/apimachinery/pkg/types" | ||||||
| 	"k8s.io/apimachinery/pkg/util/wait" | 	"k8s.io/apimachinery/pkg/util/wait" | ||||||
| 	"k8s.io/client-go/informers" | 	"k8s.io/client-go/informers" | ||||||
| @@ -35,6 +36,7 @@ import ( | |||||||
| 	"k8s.io/client-go/tools/cache" | 	"k8s.io/client-go/tools/cache" | ||||||
| 	"k8s.io/client-go/tools/record" | 	"k8s.io/client-go/tools/record" | ||||||
| 	cloudprovider "k8s.io/cloud-provider" | 	cloudprovider "k8s.io/cloud-provider" | ||||||
|  | 	"k8s.io/kubernetes/pkg/kubelet/clustertrustbundle" | ||||||
| 	"k8s.io/kubernetes/pkg/kubelet/configmap" | 	"k8s.io/kubernetes/pkg/kubelet/configmap" | ||||||
| 	"k8s.io/kubernetes/pkg/kubelet/secret" | 	"k8s.io/kubernetes/pkg/kubelet/secret" | ||||||
| 	"k8s.io/kubernetes/pkg/kubelet/token" | 	"k8s.io/kubernetes/pkg/kubelet/token" | ||||||
| @@ -55,6 +57,7 @@ func NewInitializedVolumePluginMgr( | |||||||
| 	secretManager secret.Manager, | 	secretManager secret.Manager, | ||||||
| 	configMapManager configmap.Manager, | 	configMapManager configmap.Manager, | ||||||
| 	tokenManager *token.Manager, | 	tokenManager *token.Manager, | ||||||
|  | 	clusterTrustBundleManager clustertrustbundle.Manager, | ||||||
| 	plugins []volume.VolumePlugin, | 	plugins []volume.VolumePlugin, | ||||||
| 	prober volume.DynamicPluginProber) (*volume.VolumePluginMgr, error) { | 	prober volume.DynamicPluginProber) (*volume.VolumePluginMgr, error) { | ||||||
|  |  | ||||||
| @@ -75,15 +78,16 @@ func NewInitializedVolumePluginMgr( | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	kvh := &kubeletVolumeHost{ | 	kvh := &kubeletVolumeHost{ | ||||||
| 		kubelet:          kubelet, | 		kubelet:                   kubelet, | ||||||
| 		volumePluginMgr:  volume.VolumePluginMgr{}, | 		volumePluginMgr:           volume.VolumePluginMgr{}, | ||||||
| 		secretManager:    secretManager, | 		secretManager:             secretManager, | ||||||
| 		configMapManager: configMapManager, | 		configMapManager:          configMapManager, | ||||||
| 		tokenManager:     tokenManager, | 		tokenManager:              tokenManager, | ||||||
| 		informerFactory:  informerFactory, | 		clusterTrustBundleManager: clusterTrustBundleManager, | ||||||
| 		csiDriverLister:  csiDriverLister, | 		informerFactory:           informerFactory, | ||||||
| 		csiDriversSynced: csiDriversSynced, | 		csiDriverLister:           csiDriverLister, | ||||||
| 		exec:             utilexec.New(), | 		csiDriversSynced:          csiDriversSynced, | ||||||
|  | 		exec:                      utilexec.New(), | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if err := kvh.volumePluginMgr.InitPlugins(plugins, prober, kvh); err != nil { | 	if err := kvh.volumePluginMgr.InitPlugins(plugins, prober, kvh); err != nil { | ||||||
| @@ -104,15 +108,16 @@ func (kvh *kubeletVolumeHost) GetPluginDir(pluginName string) string { | |||||||
| } | } | ||||||
|  |  | ||||||
| type kubeletVolumeHost struct { | type kubeletVolumeHost struct { | ||||||
| 	kubelet          *Kubelet | 	kubelet                   *Kubelet | ||||||
| 	volumePluginMgr  volume.VolumePluginMgr | 	volumePluginMgr           volume.VolumePluginMgr | ||||||
| 	secretManager    secret.Manager | 	secretManager             secret.Manager | ||||||
| 	tokenManager     *token.Manager | 	tokenManager              *token.Manager | ||||||
| 	configMapManager configmap.Manager | 	configMapManager          configmap.Manager | ||||||
| 	informerFactory  informers.SharedInformerFactory | 	clusterTrustBundleManager clustertrustbundle.Manager | ||||||
| 	csiDriverLister  storagelisters.CSIDriverLister | 	informerFactory           informers.SharedInformerFactory | ||||||
| 	csiDriversSynced cache.InformerSynced | 	csiDriverLister           storagelisters.CSIDriverLister | ||||||
| 	exec             utilexec.Interface | 	csiDriversSynced          cache.InformerSynced | ||||||
|  | 	exec                      utilexec.Interface | ||||||
| } | } | ||||||
|  |  | ||||||
| func (kvh *kubeletVolumeHost) SetKubeletError(err error) { | func (kvh *kubeletVolumeHost) SetKubeletError(err error) { | ||||||
| @@ -266,6 +271,14 @@ func (kvh *kubeletVolumeHost) DeleteServiceAccountTokenFunc() func(podUID types. | |||||||
| 	return kvh.tokenManager.DeleteServiceAccountToken | 	return kvh.tokenManager.DeleteServiceAccountToken | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (kvh *kubeletVolumeHost) GetTrustAnchorsByName(name string, allowMissing bool) ([]byte, error) { | ||||||
|  | 	return kvh.clusterTrustBundleManager.GetTrustAnchorsByName(name, allowMissing) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (kvh *kubeletVolumeHost) GetTrustAnchorsBySigner(signerName string, labelSelector *metav1.LabelSelector, allowMissing bool) ([]byte, error) { | ||||||
|  | 	return kvh.clusterTrustBundleManager.GetTrustAnchorsBySigner(signerName, labelSelector, allowMissing) | ||||||
|  | } | ||||||
|  |  | ||||||
| func (kvh *kubeletVolumeHost) GetNodeLabels() (map[string]string, error) { | func (kvh *kubeletVolumeHost) GetNodeLabels() (map[string]string, error) { | ||||||
| 	node, err := kvh.kubelet.GetNode() | 	node, err := kvh.kubelet.GetNode() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|   | |||||||
| @@ -333,6 +333,13 @@ type KubeletVolumeHost interface { | |||||||
| 	WaitForCacheSync() error | 	WaitForCacheSync() error | ||||||
| 	// Returns hostutil.HostUtils | 	// Returns hostutil.HostUtils | ||||||
| 	GetHostUtil() hostutil.HostUtils | 	GetHostUtil() hostutil.HostUtils | ||||||
|  |  | ||||||
|  | 	// Returns trust anchors from the named ClusterTrustBundle. | ||||||
|  | 	GetTrustAnchorsByName(name string, allowMissing bool) ([]byte, error) | ||||||
|  |  | ||||||
|  | 	// Returns trust anchors from the ClusterTrustBundles selected by signer | ||||||
|  | 	// name and label selector. | ||||||
|  | 	GetTrustAnchorsBySigner(signerName string, labelSelector *metav1.LabelSelector, allowMissing bool) ([]byte, error) | ||||||
| } | } | ||||||
|  |  | ||||||
| // AttachDetachVolumeHost is a AttachDetach Controller specific interface that plugins can use | // AttachDetachVolumeHost is a AttachDetach Controller specific interface that plugins can use | ||||||
|   | |||||||
| @@ -45,6 +45,7 @@ const ( | |||||||
|  |  | ||||||
| type projectedPlugin struct { | type projectedPlugin struct { | ||||||
| 	host                      volume.VolumeHost | 	host                      volume.VolumeHost | ||||||
|  | 	kvHost                    volume.KubeletVolumeHost | ||||||
| 	getSecret                 func(namespace, name string) (*v1.Secret, error) | 	getSecret                 func(namespace, name string) (*v1.Secret, error) | ||||||
| 	getConfigMap              func(namespace, name string) (*v1.ConfigMap, error) | 	getConfigMap              func(namespace, name string) (*v1.ConfigMap, error) | ||||||
| 	getServiceAccountToken    func(namespace, name string, tr *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) | 	getServiceAccountToken    func(namespace, name string, tr *authenticationv1.TokenRequest) (*authenticationv1.TokenRequest, error) | ||||||
| @@ -69,6 +70,7 @@ func getPath(uid types.UID, volName string, host volume.VolumeHost) string { | |||||||
|  |  | ||||||
| func (plugin *projectedPlugin) Init(host volume.VolumeHost) error { | func (plugin *projectedPlugin) Init(host volume.VolumeHost) error { | ||||||
| 	plugin.host = host | 	plugin.host = host | ||||||
|  | 	plugin.kvHost = host.(volume.KubeletVolumeHost) | ||||||
| 	plugin.getSecret = host.GetSecretFunc() | 	plugin.getSecret = host.GetSecretFunc() | ||||||
| 	plugin.getConfigMap = host.GetConfigMapFunc() | 	plugin.getConfigMap = host.GetConfigMapFunc() | ||||||
| 	plugin.getServiceAccountToken = host.GetServiceAccountTokenFunc() | 	plugin.getServiceAccountToken = host.GetServiceAccountTokenFunc() | ||||||
| @@ -353,6 +355,42 @@ func (s *projectedVolumeMounter) collectData(mounterArgs volume.MounterArgs) (ma | |||||||
| 				Mode:   mode, | 				Mode:   mode, | ||||||
| 				FsUser: mounterArgs.FsUser, | 				FsUser: mounterArgs.FsUser, | ||||||
| 			} | 			} | ||||||
|  | 		case source.ClusterTrustBundle != nil: | ||||||
|  | 			allowEmpty := false | ||||||
|  | 			if source.ClusterTrustBundle.Optional != nil && *source.ClusterTrustBundle.Optional { | ||||||
|  | 				allowEmpty = true | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			var trustAnchors []byte | ||||||
|  | 			if source.ClusterTrustBundle.Name != nil { | ||||||
|  | 				var err error | ||||||
|  | 				trustAnchors, err = s.plugin.kvHost.GetTrustAnchorsByName(*source.ClusterTrustBundle.Name, allowEmpty) | ||||||
|  | 				if err != nil { | ||||||
|  | 					errlist = append(errlist, err) | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  | 			} else if source.ClusterTrustBundle.SignerName != nil { | ||||||
|  | 				var err error | ||||||
|  | 				trustAnchors, err = s.plugin.kvHost.GetTrustAnchorsBySigner(*source.ClusterTrustBundle.SignerName, source.ClusterTrustBundle.LabelSelector, allowEmpty) | ||||||
|  | 				if err != nil { | ||||||
|  | 					errlist = append(errlist, err) | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  | 			} else { | ||||||
|  | 				errlist = append(errlist, fmt.Errorf("ClusterTrustBundle projection requires either name or signerName to be set")) | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			mode := *s.source.DefaultMode | ||||||
|  | 			if mounterArgs.FsUser != nil || mounterArgs.FsGroup != nil { | ||||||
|  | 				mode = 0600 | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			payload[source.ClusterTrustBundle.Path] = volumeutil.FileProjection{ | ||||||
|  | 				Data:   trustAnchors, | ||||||
|  | 				Mode:   mode, | ||||||
|  | 				FsUser: mounterArgs.FsUser, | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	return payload, utilerrors.NewAggregate(errlist) | 	return payload, utilerrors.NewAggregate(errlist) | ||||||
|   | |||||||
| @@ -17,7 +17,13 @@ limitations under the License. | |||||||
| package projected | package projected | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"crypto/ed25519" | ||||||
|  | 	"crypto/rand" | ||||||
|  | 	"crypto/x509" | ||||||
|  | 	"crypto/x509/pkix" | ||||||
|  | 	"encoding/pem" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"math/big" | ||||||
| 	"os" | 	"os" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| 	"reflect" | 	"reflect" | ||||||
| @@ -26,6 +32,7 @@ import ( | |||||||
|  |  | ||||||
| 	"github.com/google/go-cmp/cmp" | 	"github.com/google/go-cmp/cmp" | ||||||
| 	authenticationv1 "k8s.io/api/authentication/v1" | 	authenticationv1 "k8s.io/api/authentication/v1" | ||||||
|  | 	certificatesv1alpha1 "k8s.io/api/certificates/v1alpha1" | ||||||
| 	v1 "k8s.io/api/core/v1" | 	v1 "k8s.io/api/core/v1" | ||||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
| 	"k8s.io/apimachinery/pkg/runtime" | 	"k8s.io/apimachinery/pkg/runtime" | ||||||
| @@ -872,13 +879,172 @@ func TestCollectDataWithServiceAccountToken(t *testing.T) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func TestCollectDataWithClusterTrustBundle(t *testing.T) { | ||||||
|  | 	// This test is limited by the use of a fake clientset and volume host.  We | ||||||
|  | 	// can't meaningfully test that label selectors end up doing the correct | ||||||
|  | 	// thing for example. | ||||||
|  |  | ||||||
|  | 	goodCert1 := mustMakeRoot(t, "root1") | ||||||
|  |  | ||||||
|  | 	testCases := []struct { | ||||||
|  | 		name string | ||||||
|  |  | ||||||
|  | 		source  v1.ProjectedVolumeSource | ||||||
|  | 		bundles []runtime.Object | ||||||
|  |  | ||||||
|  | 		fsUser  *int64 | ||||||
|  | 		fsGroup *int64 | ||||||
|  |  | ||||||
|  | 		wantPayload map[string]util.FileProjection | ||||||
|  | 		wantErr     error | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name: "single ClusterTrustBundle by name", | ||||||
|  | 			source: v1.ProjectedVolumeSource{ | ||||||
|  | 				Sources: []v1.VolumeProjection{ | ||||||
|  | 					{ | ||||||
|  | 						ClusterTrustBundle: &v1.ClusterTrustBundleProjection{ | ||||||
|  | 							Name: utilptr.String("foo"), | ||||||
|  | 							Path: "bundle.pem", | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 				DefaultMode: utilptr.Int32(0644), | ||||||
|  | 			}, | ||||||
|  | 			bundles: []runtime.Object{ | ||||||
|  | 				&certificatesv1alpha1.ClusterTrustBundle{ | ||||||
|  | 					ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 						Name: "foo", | ||||||
|  | 					}, | ||||||
|  | 					Spec: certificatesv1alpha1.ClusterTrustBundleSpec{ | ||||||
|  | 						TrustBundle: string(goodCert1), | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			wantPayload: map[string]util.FileProjection{ | ||||||
|  | 				"bundle.pem": { | ||||||
|  | 					Data: []byte(goodCert1), | ||||||
|  | 					Mode: 0644, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "single ClusterTrustBundle by signer name", | ||||||
|  | 			source: v1.ProjectedVolumeSource{ | ||||||
|  | 				Sources: []v1.VolumeProjection{ | ||||||
|  | 					{ | ||||||
|  | 						ClusterTrustBundle: &v1.ClusterTrustBundleProjection{ | ||||||
|  | 							SignerName: utilptr.String("foo.example/bar"), // Note: fake client doesn't understand selection by signer name. | ||||||
|  | 							LabelSelector: &metav1.LabelSelector{ | ||||||
|  | 								MatchLabels: map[string]string{ | ||||||
|  | 									"key": "non-value", // Note: fake client doesn't actually act on label selectors. | ||||||
|  | 								}, | ||||||
|  | 							}, | ||||||
|  | 							Path: "bundle.pem", | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 				DefaultMode: utilptr.Int32(0644), | ||||||
|  | 			}, | ||||||
|  | 			bundles: []runtime.Object{ | ||||||
|  | 				&certificatesv1alpha1.ClusterTrustBundle{ | ||||||
|  | 					ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 						Name: "foo:example:bar", | ||||||
|  | 						Labels: map[string]string{ | ||||||
|  | 							"key": "value", | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 					Spec: certificatesv1alpha1.ClusterTrustBundleSpec{ | ||||||
|  | 						SignerName:  "foo.example/bar", | ||||||
|  | 						TrustBundle: string(goodCert1), | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			wantPayload: map[string]util.FileProjection{ | ||||||
|  | 				"bundle.pem": { | ||||||
|  | 					Data: []byte(goodCert1), | ||||||
|  | 					Mode: 0644, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "single ClusterTrustBundle by name, non-default mode", | ||||||
|  | 			source: v1.ProjectedVolumeSource{ | ||||||
|  | 				Sources: []v1.VolumeProjection{ | ||||||
|  | 					{ | ||||||
|  | 						ClusterTrustBundle: &v1.ClusterTrustBundleProjection{ | ||||||
|  | 							Name: utilptr.String("foo"), | ||||||
|  | 							Path: "bundle.pem", | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 				DefaultMode: utilptr.Int32(0600), | ||||||
|  | 			}, | ||||||
|  | 			bundles: []runtime.Object{ | ||||||
|  | 				&certificatesv1alpha1.ClusterTrustBundle{ | ||||||
|  | 					ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 						Name: "foo", | ||||||
|  | 					}, | ||||||
|  | 					Spec: certificatesv1alpha1.ClusterTrustBundleSpec{ | ||||||
|  | 						TrustBundle: string(goodCert1), | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			wantPayload: map[string]util.FileProjection{ | ||||||
|  | 				"bundle.pem": { | ||||||
|  | 					Data: []byte(goodCert1), | ||||||
|  | 					Mode: 0600, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, tc := range testCases { | ||||||
|  | 		t.Run(tc.name, func(t *testing.T) { | ||||||
|  | 			pod := &v1.Pod{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Namespace: "default", | ||||||
|  | 					UID:       types.UID("test_pod_uid"), | ||||||
|  | 				}, | ||||||
|  | 				Spec: v1.PodSpec{ServiceAccountName: "foo"}, | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			client := fake.NewSimpleClientset(tc.bundles...) | ||||||
|  |  | ||||||
|  | 			tempDir, host := newTestHost(t, client) | ||||||
|  | 			defer os.RemoveAll(tempDir) | ||||||
|  |  | ||||||
|  | 			var myVolumeMounter = projectedVolumeMounter{ | ||||||
|  | 				projectedVolume: &projectedVolume{ | ||||||
|  | 					sources: tc.source.Sources, | ||||||
|  | 					podUID:  pod.UID, | ||||||
|  | 					plugin: &projectedPlugin{ | ||||||
|  | 						host:   host, | ||||||
|  | 						kvHost: host.(volume.KubeletVolumeHost), | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 				source: tc.source, | ||||||
|  | 				pod:    pod, | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			gotPayload, err := myVolumeMounter.collectData(volume.MounterArgs{FsUser: tc.fsUser, FsGroup: tc.fsGroup}) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatalf("Unexpected failure making payload: %v", err) | ||||||
|  | 			} | ||||||
|  | 			if diff := cmp.Diff(tc.wantPayload, gotPayload); diff != "" { | ||||||
|  | 				t.Fatalf("Bad payload; diff (-want +got)\n%s", diff) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| func newTestHost(t *testing.T, clientset clientset.Interface) (string, volume.VolumeHost) { | func newTestHost(t *testing.T, clientset clientset.Interface) (string, volume.VolumeHost) { | ||||||
| 	tempDir, err := os.MkdirTemp("", "projected_volume_test.") | 	tempDir, err := os.MkdirTemp("", "projected_volume_test.") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("can't make a temp rootdir: %v", err) | 		t.Fatalf("can't make a temp rootdir: %v", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return tempDir, volumetest.NewFakeVolumeHost(t, tempDir, clientset, emptydir.ProbeVolumePlugins()) | 	return tempDir, volumetest.NewFakeKubeletVolumeHost(t, tempDir, clientset, emptydir.ProbeVolumePlugins()) | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestCanSupport(t *testing.T) { | func TestCanSupport(t *testing.T) { | ||||||
| @@ -1322,3 +1488,30 @@ func doTestCleanAndTeardown(plugin volume.VolumePlugin, podUID types.UID, testVo | |||||||
| 		t.Errorf("TearDown() failed: %v", err) | 		t.Errorf("TearDown() failed: %v", err) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func mustMakeRoot(t *testing.T, cn string) string { | ||||||
|  | 	pub, priv, err := ed25519.GenerateKey(rand.Reader) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Error while generating key: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	template := &x509.Certificate{ | ||||||
|  | 		SerialNumber: big.NewInt(0), | ||||||
|  | 		Subject: pkix.Name{ | ||||||
|  | 			CommonName: cn, | ||||||
|  | 		}, | ||||||
|  | 		IsCA:                  true, | ||||||
|  | 		BasicConstraintsValid: true, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	cert, err := x509.CreateCertificate(rand.Reader, template, template, pub, priv) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Error while making certificate: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return string(pem.EncodeToMemory(&pem.Block{ | ||||||
|  | 		Type:    "CERTIFICATE", | ||||||
|  | 		Headers: nil, | ||||||
|  | 		Bytes:   cert, | ||||||
|  | 	})) | ||||||
|  | } | ||||||
|   | |||||||
| @@ -17,6 +17,7 @@ limitations under the License. | |||||||
| package testing | package testing | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"bytes" | ||||||
| 	"context" | 	"context" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net" | 	"net" | ||||||
| @@ -437,3 +438,30 @@ func (f *fakeKubeletVolumeHost) WaitForCacheSync() error { | |||||||
| func (f *fakeKubeletVolumeHost) GetHostUtil() hostutil.HostUtils { | func (f *fakeKubeletVolumeHost) GetHostUtil() hostutil.HostUtils { | ||||||
| 	return f.hostUtil | 	return f.hostUtil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (f *fakeKubeletVolumeHost) GetTrustAnchorsByName(name string, allowMissing bool) ([]byte, error) { | ||||||
|  | 	ctb, err := f.kubeClient.CertificatesV1alpha1().ClusterTrustBundles().Get(context.Background(), name, metav1.GetOptions{}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("while getting ClusterTrustBundle %s: %w", name, err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return []byte(ctb.Spec.TrustBundle), nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Note: we do none of the deduplication and sorting that the real deal should do. | ||||||
|  | func (f *fakeKubeletVolumeHost) GetTrustAnchorsBySigner(signerName string, labelSelector *metav1.LabelSelector, allowMissing bool) ([]byte, error) { | ||||||
|  | 	ctbList, err := f.kubeClient.CertificatesV1alpha1().ClusterTrustBundles().List(context.Background(), metav1.ListOptions{}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("while listing all ClusterTrustBundles: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	fullSet := bytes.Buffer{} | ||||||
|  | 	for i, ctb := range ctbList.Items { | ||||||
|  | 		fullSet.WriteString(ctb.Spec.TrustBundle) | ||||||
|  | 		if i != len(ctbList.Items)-1 { | ||||||
|  | 			fullSet.WriteString("\n") | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return fullSet.Bytes(), nil | ||||||
|  | } | ||||||
|   | |||||||
| @@ -258,6 +258,17 @@ func (p *Plugin) admitPodCreate(nodeName string, a admission.Attributes) error { | |||||||
| 	if hasConfigMaps { | 	if hasConfigMaps { | ||||||
| 		return admission.NewForbidden(a, fmt.Errorf("node %q can not create pods that reference configmaps", nodeName)) | 		return admission.NewForbidden(a, fmt.Errorf("node %q can not create pods that reference configmaps", nodeName)) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	for _, vol := range pod.Spec.Volumes { | ||||||
|  | 		if vol.VolumeSource.Projected != nil { | ||||||
|  | 			for _, src := range vol.VolumeSource.Projected.Sources { | ||||||
|  | 				if src.ClusterTrustBundle != nil { | ||||||
|  | 					return admission.NewForbidden(a, fmt.Errorf("node %q can not create pods that reference clustertrustbundles", nodeName)) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	for _, v := range pod.Spec.Volumes { | 	for _, v := range pod.Spec.Volumes { | ||||||
| 		if v.PersistentVolumeClaim != nil { | 		if v.PersistentVolumeClaim != nil { | ||||||
| 			return admission.NewForbidden(a, fmt.Errorf("node %q can not create pods that reference persistentvolumeclaims", nodeName)) | 			return admission.NewForbidden(a, fmt.Errorf("node %q can not create pods that reference persistentvolumeclaims", nodeName)) | ||||||
|   | |||||||
| @@ -394,6 +394,9 @@ func Test_nodePlugin_Admit(t *testing.T) { | |||||||
| 	configmappod, _ := makeTestPod("ns", "myconfigmappod", "mynode", true) | 	configmappod, _ := makeTestPod("ns", "myconfigmappod", "mynode", true) | ||||||
| 	configmappod.Spec.Volumes = []api.Volume{{VolumeSource: api.VolumeSource{ConfigMap: &api.ConfigMapVolumeSource{LocalObjectReference: api.LocalObjectReference{Name: "foo"}}}}} | 	configmappod.Spec.Volumes = []api.Volume{{VolumeSource: api.VolumeSource{ConfigMap: &api.ConfigMapVolumeSource{LocalObjectReference: api.LocalObjectReference{Name: "foo"}}}}} | ||||||
|  |  | ||||||
|  | 	ctbpod, _ := makeTestPod("ns", "myctbpod", "mynode", true) | ||||||
|  | 	ctbpod.Spec.Volumes = []api.Volume{{VolumeSource: api.VolumeSource{Projected: &api.ProjectedVolumeSource{Sources: []api.VolumeProjection{{ClusterTrustBundle: &api.ClusterTrustBundleProjection{Name: pointer.String("foo")}}}}}}} | ||||||
|  |  | ||||||
| 	pvcpod, _ := makeTestPod("ns", "mypvcpod", "mynode", true) | 	pvcpod, _ := makeTestPod("ns", "mypvcpod", "mynode", true) | ||||||
| 	pvcpod.Spec.Volumes = []api.Volume{{VolumeSource: api.VolumeSource{PersistentVolumeClaim: &api.PersistentVolumeClaimVolumeSource{ClaimName: "foo"}}}} | 	pvcpod.Spec.Volumes = []api.Volume{{VolumeSource: api.VolumeSource{PersistentVolumeClaim: &api.PersistentVolumeClaimVolumeSource{ClaimName: "foo"}}}} | ||||||
|  |  | ||||||
| @@ -866,6 +869,12 @@ func Test_nodePlugin_Admit(t *testing.T) { | |||||||
| 			attributes: admission.NewAttributesRecord(configmappod, nil, podKind, configmappod.Namespace, configmappod.Name, podResource, "", admission.Create, &metav1.CreateOptions{}, false, mynode), | 			attributes: admission.NewAttributesRecord(configmappod, nil, podKind, configmappod.Namespace, configmappod.Name, podResource, "", admission.Create, &metav1.CreateOptions{}, false, mynode), | ||||||
| 			err:        "reference configmaps", | 			err:        "reference configmaps", | ||||||
| 		}, | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:       "forbid create of pod referencing clustertrustbundle", | ||||||
|  | 			podsGetter: noExistingPods, | ||||||
|  | 			attributes: admission.NewAttributesRecord(ctbpod, nil, podKind, ctbpod.Namespace, ctbpod.Name, podResource, "", admission.Create, &metav1.CreateOptions{}, false, mynode), | ||||||
|  | 			err:        "reference clustertrustbundles", | ||||||
|  | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			name:       "forbid create of pod referencing persistentvolumeclaim", | 			name:       "forbid create of pod referencing persistentvolumeclaim", | ||||||
| 			podsGetter: noExistingPods, | 			podsGetter: noExistingPods, | ||||||
|   | |||||||
| @@ -210,9 +210,6 @@ func (s *Plugin) Validate(ctx context.Context, a admission.Attributes, o admissi | |||||||
| 					if projSource.ServiceAccountToken != nil { | 					if projSource.ServiceAccountToken != nil { | ||||||
| 						return admission.NewForbidden(a, fmt.Errorf("a mirror pod may not use ServiceAccountToken volume projections")) | 						return admission.NewForbidden(a, fmt.Errorf("a mirror pod may not use ServiceAccountToken volume projections")) | ||||||
| 					} | 					} | ||||||
| 					if projSource.ClusterTrustBundle != nil { |  | ||||||
| 						return admission.NewForbidden(a, fmt.Errorf("a mirror pod may not use ClusterTrustBundle volume projections")) |  | ||||||
| 					} |  | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user