Define ClusterTrustBundlePEM projected volume
This commit defines the ClusterTrustBundlePEM projected volume types. These types have been renamed from the KEP (PEMTrustAnchors) in order to leave open the possibility of a similar projection drawing from a yet-to-exist namespaced-scoped TrustBundle object, which came up during KEP discussion. * Add the projection field to internal and v1 APIs. * Add validation to ensure that usages of the project must specify a name and path. * Add TODO covering admission control to forbid mirror pods from using the projection. Part of KEP-3257.
This commit is contained in:
		| @@ -549,6 +549,7 @@ func dropDisabledFields( | ||||
| 	dropDisabledMatchLabelKeysFieldInTopologySpread(podSpec, oldPodSpec) | ||||
| 	dropDisabledMatchLabelKeysFieldInPodAffinity(podSpec, oldPodSpec) | ||||
| 	dropDisabledDynamicResourceAllocationFields(podSpec, oldPodSpec) | ||||
| 	dropDisabledClusterTrustBundleProjection(podSpec, oldPodSpec) | ||||
|  | ||||
| 	if !utilfeature.DefaultFeatureGate.Enabled(features.InPlacePodVerticalScaling) && !inPlacePodVerticalScalingInUse(oldPodSpec) { | ||||
| 		// Drop ResizePolicy fields. Don't drop updates to Resources field as template.spec.resources | ||||
| @@ -969,6 +970,53 @@ func restartableInitContainersInUse(podSpec *api.PodSpec) bool { | ||||
| 	return inUse | ||||
| } | ||||
|  | ||||
| func clusterTrustBundleProjectionInUse(podSpec *api.PodSpec) bool { | ||||
| 	if podSpec == nil { | ||||
| 		return false | ||||
| 	} | ||||
| 	for _, v := range podSpec.Volumes { | ||||
| 		if v.Projected == nil { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		for _, s := range v.Projected.Sources { | ||||
| 			if s.ClusterTrustBundle != nil { | ||||
| 				return true | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| func dropDisabledClusterTrustBundleProjection(podSpec, oldPodSpec *api.PodSpec) { | ||||
| 	if utilfeature.DefaultFeatureGate.Enabled(features.ClusterTrustBundleProjection) { | ||||
| 		return | ||||
| 	} | ||||
| 	if podSpec == nil { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	// If the pod was already using it, it can keep using it. | ||||
| 	if clusterTrustBundleProjectionInUse(oldPodSpec) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	for _, v := range podSpec.Volumes { | ||||
| 		if v.Projected == nil { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		filteredSources := []api.VolumeProjection{} | ||||
| 		for _, s := range v.Projected.Sources { | ||||
| 			if s.ClusterTrustBundle == nil { | ||||
| 				filteredSources = append(filteredSources, s) | ||||
| 			} | ||||
| 		} | ||||
| 		v.Projected.Sources = filteredSources | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func hasInvalidLabelValueInAffinitySelector(spec *api.PodSpec) bool { | ||||
| 	if spec.Affinity != nil { | ||||
| 		if spec.Affinity.PodAffinity != nil { | ||||
|   | ||||
| @@ -21,14 +21,11 @@ import ( | ||||
| 	"crypto/x509" | ||||
| 	"encoding/pem" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/google/go-cmp/cmp" | ||||
| 	v1 "k8s.io/api/core/v1" | ||||
| 	apiequality "k8s.io/apimachinery/pkg/api/equality" | ||||
| 	apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation" | ||||
| 	"k8s.io/apimachinery/pkg/util/sets" | ||||
| 	utilvalidation "k8s.io/apimachinery/pkg/util/validation" | ||||
| 	"k8s.io/apimachinery/pkg/util/validation/field" | ||||
| 	utilcert "k8s.io/client-go/util/cert" | ||||
| 	"k8s.io/kubernetes/pkg/apis/certificates" | ||||
| @@ -198,7 +195,7 @@ func validateCertificateSigningRequest(csr *certificates.CertificateSigningReque | ||||
| 	if !opts.allowLegacySignerName && csr.Spec.SignerName == certificates.LegacyUnknownSignerName { | ||||
| 		allErrs = append(allErrs, field.Invalid(specPath.Child("signerName"), csr.Spec.SignerName, "the legacy signerName is not allowed via this API version")) | ||||
| 	} else { | ||||
| 		allErrs = append(allErrs, ValidateSignerName(specPath.Child("signerName"), csr.Spec.SignerName)...) | ||||
| 		allErrs = append(allErrs, apivalidation.ValidateSignerName(specPath.Child("signerName"), csr.Spec.SignerName)...) | ||||
| 	} | ||||
| 	if csr.Spec.ExpirationSeconds != nil && *csr.Spec.ExpirationSeconds < 600 { | ||||
| 		allErrs = append(allErrs, field.Invalid(specPath.Child("expirationSeconds"), *csr.Spec.ExpirationSeconds, "may not specify a duration less than 600 seconds (10 minutes)")) | ||||
| @@ -266,82 +263,6 @@ func validateConditions(fldPath *field.Path, csr *certificates.CertificateSignin | ||||
| 	return allErrs | ||||
| } | ||||
|  | ||||
| // ensure signerName is of the form domain.com/something and up to 571 characters. | ||||
| // This length and format is specified to accommodate signerNames like: | ||||
| // <fqdn>/<resource-namespace>.<resource-name>. | ||||
| // The max length of a FQDN is 253 characters (DNS1123Subdomain max length) | ||||
| // The max length of a namespace name is 63 characters (DNS1123Label max length) | ||||
| // The max length of a resource name is 253 characters (DNS1123Subdomain max length) | ||||
| // We then add an additional 2 characters to account for the one '.' and one '/'. | ||||
| func ValidateSignerName(fldPath *field.Path, signerName string) field.ErrorList { | ||||
| 	var el field.ErrorList | ||||
| 	if len(signerName) == 0 { | ||||
| 		el = append(el, field.Required(fldPath, "")) | ||||
| 		return el | ||||
| 	} | ||||
|  | ||||
| 	segments := strings.Split(signerName, "/") | ||||
| 	// validate that there is one '/' in the signerName. | ||||
| 	// we do this after validating the domain segment to provide more info to the user. | ||||
| 	if len(segments) != 2 { | ||||
| 		el = append(el, field.Invalid(fldPath, signerName, "must be a fully qualified domain and path of the form 'example.com/signer-name'")) | ||||
| 		// return early here as we should not continue attempting to validate a missing or malformed path segment | ||||
| 		// (i.e. one containing multiple or zero `/`) | ||||
| 		return el | ||||
| 	} | ||||
|  | ||||
| 	// validate that segments[0] is less than 253 characters altogether | ||||
| 	maxDomainSegmentLength := utilvalidation.DNS1123SubdomainMaxLength | ||||
| 	if len(segments[0]) > maxDomainSegmentLength { | ||||
| 		el = append(el, field.TooLong(fldPath, segments[0], maxDomainSegmentLength)) | ||||
| 	} | ||||
| 	// validate that segments[0] consists of valid DNS1123 labels separated by '.' | ||||
| 	domainLabels := strings.Split(segments[0], ".") | ||||
| 	for _, lbl := range domainLabels { | ||||
| 		// use IsDNS1123Label as we want to ensure the max length of any single label in the domain | ||||
| 		// is 63 characters | ||||
| 		if errs := utilvalidation.IsDNS1123Label(lbl); len(errs) > 0 { | ||||
| 			for _, err := range errs { | ||||
| 				el = append(el, field.Invalid(fldPath, segments[0], fmt.Sprintf("validating label %q: %s", lbl, err))) | ||||
| 			} | ||||
| 			// if we encounter any errors whilst parsing the domain segment, break from | ||||
| 			// validation as any further error messages will be duplicates, and non-distinguishable | ||||
| 			// from each other, confusing users. | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// validate that there is at least one '.' in segments[0] | ||||
| 	if len(domainLabels) < 2 { | ||||
| 		el = append(el, field.Invalid(fldPath, segments[0], "should be a domain with at least two segments separated by dots")) | ||||
| 	} | ||||
|  | ||||
| 	// validate that segments[1] consists of valid DNS1123 subdomains separated by '.'. | ||||
| 	pathLabels := strings.Split(segments[1], ".") | ||||
| 	for _, lbl := range pathLabels { | ||||
| 		// use IsDNS1123Subdomain because it enforces a length restriction of 253 characters | ||||
| 		// which is required in order to fit a full resource name into a single 'label' | ||||
| 		if errs := utilvalidation.IsDNS1123Subdomain(lbl); len(errs) > 0 { | ||||
| 			for _, err := range errs { | ||||
| 				el = append(el, field.Invalid(fldPath, segments[1], fmt.Sprintf("validating label %q: %s", lbl, err))) | ||||
| 			} | ||||
| 			// if we encounter any errors whilst parsing the path segment, break from | ||||
| 			// validation as any further error messages will be duplicates, and non-distinguishable | ||||
| 			// from each other, confusing users. | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// ensure that segments[1] can accommodate a dns label + dns subdomain + '.' | ||||
| 	maxPathSegmentLength := utilvalidation.DNS1123SubdomainMaxLength + utilvalidation.DNS1123LabelMaxLength + 1 | ||||
| 	maxSignerNameLength := maxDomainSegmentLength + maxPathSegmentLength + 1 | ||||
| 	if len(signerName) > maxSignerNameLength { | ||||
| 		el = append(el, field.TooLong(fldPath, signerName, maxSignerNameLength)) | ||||
| 	} | ||||
|  | ||||
| 	return el | ||||
| } | ||||
|  | ||||
| func ValidateCertificateSigningRequestUpdate(newCSR, oldCSR *certificates.CertificateSigningRequest) field.ErrorList { | ||||
| 	opts := getValidationOptions(newCSR, oldCSR) | ||||
| 	return validateCertificateSigningRequestUpdate(newCSR, oldCSR, opts) | ||||
| @@ -539,24 +460,6 @@ func hasDuplicateUsage(usages []certificates.KeyUsage) bool { | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| // We require your name to be prefixed by .spec.signerName | ||||
| func validateClusterTrustBundleName(signerName string) func(name string, prefix bool) []string { | ||||
| 	return func(name string, isPrefix bool) []string { | ||||
| 		if signerName == "" { | ||||
| 			if strings.Contains(name, ":") { | ||||
| 				return []string{"ClusterTrustBundle without signer name must not have \":\" in its name"} | ||||
| 			} | ||||
| 			return apimachineryvalidation.NameIsDNSSubdomain(name, isPrefix) | ||||
| 		} | ||||
|  | ||||
| 		requiredPrefix := strings.ReplaceAll(signerName, "/", ":") + ":" | ||||
| 		if !strings.HasPrefix(name, requiredPrefix) { | ||||
| 			return []string{fmt.Sprintf("ClusterTrustBundle for signerName %s must be named with prefix %s", signerName, requiredPrefix)} | ||||
| 		} | ||||
| 		return apimachineryvalidation.NameIsDNSSubdomain(strings.TrimPrefix(name, requiredPrefix), isPrefix) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| type ValidateClusterTrustBundleOptions struct { | ||||
| 	SuppressBundleParsing bool | ||||
| } | ||||
| @@ -565,11 +468,11 @@ type ValidateClusterTrustBundleOptions struct { | ||||
| func ValidateClusterTrustBundle(bundle *certificates.ClusterTrustBundle, opts ValidateClusterTrustBundleOptions) field.ErrorList { | ||||
| 	var allErrors field.ErrorList | ||||
|  | ||||
| 	metaErrors := apivalidation.ValidateObjectMeta(&bundle.ObjectMeta, false, validateClusterTrustBundleName(bundle.Spec.SignerName), field.NewPath("metadata")) | ||||
| 	metaErrors := apivalidation.ValidateObjectMeta(&bundle.ObjectMeta, false, apivalidation.ValidateClusterTrustBundleName(bundle.Spec.SignerName), field.NewPath("metadata")) | ||||
| 	allErrors = append(allErrors, metaErrors...) | ||||
|  | ||||
| 	if bundle.Spec.SignerName != "" { | ||||
| 		signerNameErrors := ValidateSignerName(field.NewPath("spec", "signerName"), bundle.Spec.SignerName) | ||||
| 		signerNameErrors := apivalidation.ValidateSignerName(field.NewPath("spec", "signerName"), bundle.Spec.SignerName) | ||||
| 		allErrors = append(allErrors, signerNameErrors...) | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -1759,6 +1759,29 @@ type ServiceAccountTokenProjection struct { | ||||
| 	Path string | ||||
| } | ||||
|  | ||||
| // ClusterTrustBundleProjection allows a pod to access the | ||||
| // `.spec.trustBundle` field of a ClusterTrustBundle object in an auto-updating | ||||
| // file. | ||||
| type ClusterTrustBundleProjection struct { | ||||
| 	// Select a single ClusterTrustBundle by object name.   Mutually-exclusive | ||||
| 	// with SignerName and LabelSelector. | ||||
| 	Name *string | ||||
|  | ||||
| 	// Select all ClusterTrustBundles for this signer that match LabelSelector. | ||||
| 	// Mutually-exclusive with Name. | ||||
| 	SignerName *string | ||||
|  | ||||
| 	// Select all ClusterTrustBundles that match this LabelSelecotr. | ||||
| 	// Mutually-exclusive with Name. | ||||
| 	LabelSelector *metav1.LabelSelector | ||||
|  | ||||
| 	// Block pod startup if the selected ClusterTrustBundle(s) aren't available? | ||||
| 	Optional *bool | ||||
|  | ||||
| 	// Relative path from the volume root to write the bundle. | ||||
| 	Path string | ||||
| } | ||||
|  | ||||
| // ProjectedVolumeSource represents a projected volume source | ||||
| type ProjectedVolumeSource struct { | ||||
| 	// list of volume projections | ||||
| @@ -1784,6 +1807,8 @@ type VolumeProjection struct { | ||||
| 	ConfigMap *ConfigMapProjection | ||||
| 	// information about the serviceAccountToken data to project | ||||
| 	ServiceAccountToken *ServiceAccountTokenProjection | ||||
| 	// information about the ClusterTrustBundle data to project | ||||
| 	ClusterTrustBundle *ClusterTrustBundleProjection | ||||
| } | ||||
|  | ||||
| // KeyToPath maps a string key to a path within a volume. | ||||
|   | ||||
							
								
								
									
										132
									
								
								pkg/apis/core/validation/names.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								pkg/apis/core/validation/names.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,132 @@ | ||||
| /* | ||||
| 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 validation | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
|  | ||||
| 	apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation" | ||||
| 	"k8s.io/apimachinery/pkg/util/validation" | ||||
| 	"k8s.io/apimachinery/pkg/util/validation/field" | ||||
| ) | ||||
|  | ||||
| // ValidateSignerName checks that signerName is syntactically valid. | ||||
| // | ||||
| // ensure signerName is of the form domain.com/something and up to 571 characters. | ||||
| // This length and format is specified to accommodate signerNames like: | ||||
| // <fqdn>/<resource-namespace>.<resource-name>. | ||||
| // The max length of a FQDN is 253 characters (DNS1123Subdomain max length) | ||||
| // The max length of a namespace name is 63 characters (DNS1123Label max length) | ||||
| // The max length of a resource name is 253 characters (DNS1123Subdomain max length) | ||||
| // We then add an additional 2 characters to account for the one '.' and one '/'. | ||||
| func ValidateSignerName(fldPath *field.Path, signerName string) field.ErrorList { | ||||
| 	var el field.ErrorList | ||||
| 	if len(signerName) == 0 { | ||||
| 		el = append(el, field.Required(fldPath, "")) | ||||
| 		return el | ||||
| 	} | ||||
|  | ||||
| 	segments := strings.Split(signerName, "/") | ||||
| 	// validate that there is one '/' in the signerName. | ||||
| 	// we do this after validating the domain segment to provide more info to the user. | ||||
| 	if len(segments) != 2 { | ||||
| 		el = append(el, field.Invalid(fldPath, signerName, "must be a fully qualified domain and path of the form 'example.com/signer-name'")) | ||||
| 		// return early here as we should not continue attempting to validate a missing or malformed path segment | ||||
| 		// (i.e. one containing multiple or zero `/`) | ||||
| 		return el | ||||
| 	} | ||||
|  | ||||
| 	// validate that segments[0] is less than 253 characters altogether | ||||
| 	maxDomainSegmentLength := validation.DNS1123SubdomainMaxLength | ||||
| 	if len(segments[0]) > maxDomainSegmentLength { | ||||
| 		el = append(el, field.TooLong(fldPath, segments[0], maxDomainSegmentLength)) | ||||
| 	} | ||||
| 	// validate that segments[0] consists of valid DNS1123 labels separated by '.' | ||||
| 	domainLabels := strings.Split(segments[0], ".") | ||||
| 	for _, lbl := range domainLabels { | ||||
| 		// use IsDNS1123Label as we want to ensure the max length of any single label in the domain | ||||
| 		// is 63 characters | ||||
| 		if errs := validation.IsDNS1123Label(lbl); len(errs) > 0 { | ||||
| 			for _, err := range errs { | ||||
| 				el = append(el, field.Invalid(fldPath, segments[0], fmt.Sprintf("validating label %q: %s", lbl, err))) | ||||
| 			} | ||||
| 			// if we encounter any errors whilst parsing the domain segment, break from | ||||
| 			// validation as any further error messages will be duplicates, and non-distinguishable | ||||
| 			// from each other, confusing users. | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// validate that there is at least one '.' in segments[0] | ||||
| 	if len(domainLabels) < 2 { | ||||
| 		el = append(el, field.Invalid(fldPath, segments[0], "should be a domain with at least two segments separated by dots")) | ||||
| 	} | ||||
|  | ||||
| 	// validate that segments[1] consists of valid DNS1123 subdomains separated by '.'. | ||||
| 	pathLabels := strings.Split(segments[1], ".") | ||||
| 	for _, lbl := range pathLabels { | ||||
| 		// use IsDNS1123Subdomain because it enforces a length restriction of 253 characters | ||||
| 		// which is required in order to fit a full resource name into a single 'label' | ||||
| 		if errs := validation.IsDNS1123Subdomain(lbl); len(errs) > 0 { | ||||
| 			for _, err := range errs { | ||||
| 				el = append(el, field.Invalid(fldPath, segments[1], fmt.Sprintf("validating label %q: %s", lbl, err))) | ||||
| 			} | ||||
| 			// if we encounter any errors whilst parsing the path segment, break from | ||||
| 			// validation as any further error messages will be duplicates, and non-distinguishable | ||||
| 			// from each other, confusing users. | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// ensure that segments[1] can accommodate a dns label + dns subdomain + '.' | ||||
| 	maxPathSegmentLength := validation.DNS1123SubdomainMaxLength + validation.DNS1123LabelMaxLength + 1 | ||||
| 	maxSignerNameLength := maxDomainSegmentLength + maxPathSegmentLength + 1 | ||||
| 	if len(signerName) > maxSignerNameLength { | ||||
| 		el = append(el, field.TooLong(fldPath, signerName, maxSignerNameLength)) | ||||
| 	} | ||||
|  | ||||
| 	return el | ||||
| } | ||||
|  | ||||
| // ValidateClusterTrustBundleName checks that a ClusterTrustBundle name conforms | ||||
| // to the rules documented on the type. | ||||
| func ValidateClusterTrustBundleName(signerName string) func(name string, prefix bool) []string { | ||||
| 	return func(name string, isPrefix bool) []string { | ||||
| 		if signerName == "" { | ||||
| 			if strings.Contains(name, ":") { | ||||
| 				return []string{"ClusterTrustBundle without signer name must not have \":\" in its name"} | ||||
| 			} | ||||
| 			return apimachineryvalidation.NameIsDNSSubdomain(name, isPrefix) | ||||
| 		} | ||||
|  | ||||
| 		requiredPrefix := strings.ReplaceAll(signerName, "/", ":") + ":" | ||||
| 		if !strings.HasPrefix(name, requiredPrefix) { | ||||
| 			return []string{fmt.Sprintf("ClusterTrustBundle for signerName %s must be named with prefix %s", signerName, requiredPrefix)} | ||||
| 		} | ||||
| 		return apimachineryvalidation.NameIsDNSSubdomain(strings.TrimPrefix(name, requiredPrefix), isPrefix) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func extractSignerNameFromClusterTrustBundleName(name string) (string, bool) { | ||||
| 	if splitPoint := strings.LastIndex(name, ":"); splitPoint != -1 { | ||||
| 		// This looks like it refers to a signerName trustbundle. | ||||
| 		return strings.ReplaceAll(name[:splitPoint], ":", "/"), true | ||||
| 	} else { | ||||
| 		return "", false | ||||
| 	} | ||||
| } | ||||
| @@ -1155,6 +1155,69 @@ func validateProjectionSources(projection *core.ProjectedVolumeSource, projectio | ||||
| 				allErrs = append(allErrs, field.Required(fldPath.Child("path"), "")) | ||||
| 			} | ||||
| 		} | ||||
| 		if projPath := srcPath.Child("clusterTrustBundlePEM"); source.ClusterTrustBundle != nil { | ||||
| 			numSources++ | ||||
|  | ||||
| 			usingName := source.ClusterTrustBundle.Name != nil | ||||
| 			usingSignerName := source.ClusterTrustBundle.SignerName != nil | ||||
|  | ||||
| 			switch { | ||||
| 			case usingName && usingSignerName: | ||||
| 				allErrs = append(allErrs, field.Invalid(projPath, source.ClusterTrustBundle, "only one of name and signerName may be used")) | ||||
| 			case usingName: | ||||
| 				if *source.ClusterTrustBundle.Name == "" { | ||||
| 					allErrs = append(allErrs, field.Required(projPath.Child("name"), "must be a valid object name")) | ||||
| 				} | ||||
|  | ||||
| 				name := *source.ClusterTrustBundle.Name | ||||
| 				if signerName, ok := extractSignerNameFromClusterTrustBundleName(name); ok { | ||||
| 					validationFunc := ValidateClusterTrustBundleName(signerName) | ||||
| 					errMsgs := validationFunc(name, false) | ||||
| 					for _, msg := range errMsgs { | ||||
| 						allErrs = append(allErrs, field.Invalid(projPath.Child("name"), name, fmt.Sprintf("not a valid clustertrustbundlename: %v", msg))) | ||||
| 					} | ||||
| 				} else { | ||||
| 					validationFunc := ValidateClusterTrustBundleName("") | ||||
| 					errMsgs := validationFunc(name, false) | ||||
| 					for _, msg := range errMsgs { | ||||
| 						allErrs = append(allErrs, field.Invalid(projPath.Child("name"), name, fmt.Sprintf("not a valid clustertrustbundlename: %v", msg))) | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| 				if source.ClusterTrustBundle.LabelSelector != nil { | ||||
| 					allErrs = append(allErrs, field.Invalid(projPath.Child("labelSelector"), source.ClusterTrustBundle.LabelSelector, "labelSelector must be unset if name is specified")) | ||||
| 				} | ||||
| 			case usingSignerName: | ||||
| 				if *source.ClusterTrustBundle.SignerName == "" { | ||||
| 					allErrs = append(allErrs, field.Required(projPath.Child("signerName"), "must be a valid signer name")) | ||||
| 				} | ||||
|  | ||||
| 				allErrs = append(allErrs, ValidateSignerName(projPath.Child("signerName"), *source.ClusterTrustBundle.SignerName)...) | ||||
|  | ||||
| 				labelSelectorErrs := unversionedvalidation.ValidateLabelSelector( | ||||
| 					source.ClusterTrustBundle.LabelSelector, | ||||
| 					unversionedvalidation.LabelSelectorValidationOptions{AllowInvalidLabelValueInSelector: false}, | ||||
| 					projPath.Child("labelSelector"), | ||||
| 				) | ||||
| 				allErrs = append(allErrs, labelSelectorErrs...) | ||||
|  | ||||
| 			default: | ||||
| 				allErrs = append(allErrs, field.Required(projPath, "either name or signerName must be specified")) | ||||
| 			} | ||||
|  | ||||
| 			if source.ClusterTrustBundle.Path == "" { | ||||
| 				allErrs = append(allErrs, field.Required(projPath.Child("path"), "")) | ||||
| 			} | ||||
|  | ||||
| 			allErrs = append(allErrs, validateLocalNonReservedPath(source.ClusterTrustBundle.Path, projPath.Child("path"))...) | ||||
|  | ||||
| 			curPath := source.ClusterTrustBundle.Path | ||||
| 			if !allPaths.Has(curPath) { | ||||
| 				allPaths.Insert(curPath) | ||||
| 			} else { | ||||
| 				allErrs = append(allErrs, field.Invalid(fldPath, curPath, "conflicting duplicate paths")) | ||||
| 			} | ||||
| 		} | ||||
| 		if numSources > 1 { | ||||
| 			allErrs = append(allErrs, field.Forbidden(srcPath, "may not specify more than 1 volume type")) | ||||
| 		} | ||||
|   | ||||
| @@ -10414,6 +10414,63 @@ func TestValidatePod(t *testing.T) { | ||||
| 				}}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"valid ClusterTrustBundlePEM projected volume referring to a CTB by name": { | ||||
| 			ObjectMeta: metav1.ObjectMeta{Name: "valid-extended", Namespace: "ns"}, | ||||
| 			Spec: core.PodSpec{ | ||||
| 				ServiceAccountName: "some-service-account", | ||||
| 				Containers:         []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, | ||||
| 				RestartPolicy:      core.RestartPolicyAlways, | ||||
| 				DNSPolicy:          core.DNSClusterFirst, | ||||
| 				Volumes: []core.Volume{ | ||||
| 					{ | ||||
| 						Name: "projected-volume", | ||||
| 						VolumeSource: core.VolumeSource{ | ||||
| 							Projected: &core.ProjectedVolumeSource{ | ||||
| 								Sources: []core.VolumeProjection{ | ||||
| 									{ | ||||
| 										ClusterTrustBundle: &core.ClusterTrustBundleProjection{ | ||||
| 											Path: "foo-path", | ||||
| 											Name: utilpointer.String("foo"), | ||||
| 										}, | ||||
| 									}, | ||||
| 								}, | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"valid ClusterTrustBundlePEM projected volume referring to a CTB by signer name": { | ||||
| 			ObjectMeta: metav1.ObjectMeta{Name: "valid-extended", Namespace: "ns"}, | ||||
| 			Spec: core.PodSpec{ | ||||
| 				ServiceAccountName: "some-service-account", | ||||
| 				Containers:         []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, | ||||
| 				RestartPolicy:      core.RestartPolicyAlways, | ||||
| 				DNSPolicy:          core.DNSClusterFirst, | ||||
| 				Volumes: []core.Volume{ | ||||
| 					{ | ||||
| 						Name: "projected-volume", | ||||
| 						VolumeSource: core.VolumeSource{ | ||||
| 							Projected: &core.ProjectedVolumeSource{ | ||||
| 								Sources: []core.VolumeProjection{ | ||||
| 									{ | ||||
| 										ClusterTrustBundle: &core.ClusterTrustBundleProjection{ | ||||
| 											Path:       "foo-path", | ||||
| 											SignerName: utilpointer.String("example.com/foo"), | ||||
| 											LabelSelector: &metav1.LabelSelector{ | ||||
| 												MatchLabels: map[string]string{ | ||||
| 													"version": "live", | ||||
| 												}, | ||||
| 											}, | ||||
| 										}, | ||||
| 									}, | ||||
| 								}, | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"ephemeral volume + PVC, no conflict between them": { | ||||
| 			ObjectMeta: metav1.ObjectMeta{Name: "123", Namespace: "ns"}, | ||||
| 			Spec: core.PodSpec{ | ||||
| @@ -12024,6 +12081,133 @@ func TestValidatePod(t *testing.T) { | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"ClusterTrustBundlePEM projected volume using both byName and bySigner": { | ||||
| 			expectedError: "only one of name and signerName may be used", | ||||
| 			spec: core.Pod{ | ||||
| 				ObjectMeta: metav1.ObjectMeta{Name: "valid-extended", Namespace: "ns"}, | ||||
| 				Spec: core.PodSpec{ | ||||
| 					ServiceAccountName: "some-service-account", | ||||
| 					Containers:         []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, | ||||
| 					RestartPolicy:      core.RestartPolicyAlways, | ||||
| 					DNSPolicy:          core.DNSClusterFirst, | ||||
| 					Volumes: []core.Volume{ | ||||
| 						{ | ||||
| 							Name: "projected-volume", | ||||
| 							VolumeSource: core.VolumeSource{ | ||||
| 								Projected: &core.ProjectedVolumeSource{ | ||||
| 									Sources: []core.VolumeProjection{ | ||||
| 										{ | ||||
| 											ClusterTrustBundle: &core.ClusterTrustBundleProjection{ | ||||
| 												Path:       "foo-path", | ||||
| 												SignerName: utilpointer.String("example.com/foo"), | ||||
| 												LabelSelector: &metav1.LabelSelector{ | ||||
| 													MatchLabels: map[string]string{ | ||||
| 														"version": "live", | ||||
| 													}, | ||||
| 												}, | ||||
| 												Name: utilpointer.String("foo"), | ||||
| 											}, | ||||
| 										}, | ||||
| 									}, | ||||
| 								}, | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"ClusterTrustBundlePEM projected volume byName with no name": { | ||||
| 			expectedError: "must be a valid object name", | ||||
| 			spec: core.Pod{ | ||||
| 				ObjectMeta: metav1.ObjectMeta{Name: "valid-extended", Namespace: "ns"}, | ||||
| 				Spec: core.PodSpec{ | ||||
| 					ServiceAccountName: "some-service-account", | ||||
| 					Containers:         []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, | ||||
| 					RestartPolicy:      core.RestartPolicyAlways, | ||||
| 					DNSPolicy:          core.DNSClusterFirst, | ||||
| 					Volumes: []core.Volume{ | ||||
| 						{ | ||||
| 							Name: "projected-volume", | ||||
| 							VolumeSource: core.VolumeSource{ | ||||
| 								Projected: &core.ProjectedVolumeSource{ | ||||
| 									Sources: []core.VolumeProjection{ | ||||
| 										{ | ||||
| 											ClusterTrustBundle: &core.ClusterTrustBundleProjection{ | ||||
| 												Path: "foo-path", | ||||
| 												Name: utilpointer.String(""), | ||||
| 											}, | ||||
| 										}, | ||||
| 									}, | ||||
| 								}, | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"ClusterTrustBundlePEM projected volume bySigner with no signer name": { | ||||
| 			expectedError: "must be a valid signer name", | ||||
| 			spec: core.Pod{ | ||||
| 				ObjectMeta: metav1.ObjectMeta{Name: "valid-extended", Namespace: "ns"}, | ||||
| 				Spec: core.PodSpec{ | ||||
| 					ServiceAccountName: "some-service-account", | ||||
| 					Containers:         []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, | ||||
| 					RestartPolicy:      core.RestartPolicyAlways, | ||||
| 					DNSPolicy:          core.DNSClusterFirst, | ||||
| 					Volumes: []core.Volume{ | ||||
| 						{ | ||||
| 							Name: "projected-volume", | ||||
| 							VolumeSource: core.VolumeSource{ | ||||
| 								Projected: &core.ProjectedVolumeSource{ | ||||
| 									Sources: []core.VolumeProjection{ | ||||
| 										{ | ||||
| 											ClusterTrustBundle: &core.ClusterTrustBundleProjection{ | ||||
| 												Path:       "foo-path", | ||||
| 												SignerName: utilpointer.String(""), | ||||
| 												LabelSelector: &metav1.LabelSelector{ | ||||
| 													MatchLabels: map[string]string{ | ||||
| 														"foo": "bar", | ||||
| 													}, | ||||
| 												}, | ||||
| 											}, | ||||
| 										}, | ||||
| 									}, | ||||
| 								}, | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"ClusterTrustBundlePEM projected volume bySigner with invalid signer name": { | ||||
| 			expectedError: "must be a fully qualified domain and path of the form", | ||||
| 			spec: core.Pod{ | ||||
| 				ObjectMeta: metav1.ObjectMeta{Name: "valid-extended", Namespace: "ns"}, | ||||
| 				Spec: core.PodSpec{ | ||||
| 					ServiceAccountName: "some-service-account", | ||||
| 					Containers:         []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, | ||||
| 					RestartPolicy:      core.RestartPolicyAlways, | ||||
| 					DNSPolicy:          core.DNSClusterFirst, | ||||
| 					Volumes: []core.Volume{ | ||||
| 						{ | ||||
| 							Name: "projected-volume", | ||||
| 							VolumeSource: core.VolumeSource{ | ||||
| 								Projected: &core.ProjectedVolumeSource{ | ||||
| 									Sources: []core.VolumeProjection{ | ||||
| 										{ | ||||
| 											ClusterTrustBundle: &core.ClusterTrustBundleProjection{ | ||||
| 												Path:       "foo-path", | ||||
| 												SignerName: utilpointer.String("example.com/foo/invalid"), | ||||
| 											}, | ||||
| 										}, | ||||
| 									}, | ||||
| 								}, | ||||
| 							}, | ||||
| 						}, | ||||
| 					}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		"final PVC name for ephemeral volume must be valid": { | ||||
| 			expectedError: "spec.volumes[1].name: Invalid value: \"" + longVolName + "\": PVC name \"" + longPodName + "-" + longVolName + "\": must be no more than 253 characters", | ||||
| 			spec: core.Pod{ | ||||
|   | ||||
| @@ -210,6 +210,9 @@ func (s *Plugin) Validate(ctx context.Context, a admission.Attributes, o admissi | ||||
| 					if projSource.ServiceAccountToken != nil { | ||||
| 						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")) | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|   | ||||
| @@ -1842,22 +1842,31 @@ type ServiceAccountTokenProjection struct { | ||||
| // filesystem. | ||||
| type ClusterTrustBundleProjection struct { | ||||
| 	// Select a single ClusterTrustBundle by object name.  Mutually-exclusive | ||||
| 	// with SignerName and LabelSelector. | ||||
| 	// with signerName and labelSelector. | ||||
| 	// +optional | ||||
| 	Name *string `json:"name,omitempty" protobuf:"bytes,1,rep,name=name"` | ||||
|  | ||||
| 	// Select all ClusterTrustBundles that match this signer name. | ||||
| 	// Mutually-exclusive with Name. | ||||
| 	// Mutually-exclusive with name.  The contents of all selected | ||||
| 	// ClusterTrustBundles will be unified and deduplicated. | ||||
| 	// +optional | ||||
| 	SignerName *string `json:"signerName,omitempty" protobuf:"bytes,2,rep,name=signerName"` | ||||
|  | ||||
| 	// Select all ClusterTrustBundles that match this label selector.  Must not | ||||
| 	// be null or empty if SignerName is provided.  Mutually-exclusive with | ||||
| 	// Name. | ||||
| 	// | ||||
| 	// Select all ClusterTrustBundles that match this label selector.  Only has | ||||
| 	// effect if signerName is set.  Mutually-exclusive with name.  If unset, | ||||
| 	// interpreted as "match nothing".  If set but empty, interpreted as "match | ||||
| 	// everything". | ||||
| 	// +optional | ||||
| 	LabelSelector *metav1.LabelSelector `json:"labelSelector,omitempty" protobuf:"bytes,3,rep,name=labelSelector"` | ||||
|  | ||||
| 	// If true, don't block pod startup if the referenced ClusterTrustBundle(s) | ||||
| 	// aren't available.  If using name, then the named ClusterTrustBundle is | ||||
| 	// allowed not to exist.  If using signerName, then the combination of | ||||
| 	// signerName and labelSelector is allowed to match zero | ||||
| 	// ClusterTrustBundles. | ||||
| 	// +optional | ||||
| 	Optional *bool `json:"optional,omitempty"` | ||||
|  | ||||
| 	// Relative path from the volume root to write the bundle. | ||||
| 	Path string `json:"path" protobuf:"bytes,4,rep,name=path"` | ||||
| } | ||||
| @@ -1895,26 +1904,20 @@ type VolumeProjection struct { | ||||
| 	ServiceAccountToken *ServiceAccountTokenProjection `json:"serviceAccountToken,omitempty" protobuf:"bytes,4,opt,name=serviceAccountToken"` | ||||
|  | ||||
| 	// ClusterTrustBundle allows a pod to access the `.spec.trustBundle` field | ||||
| 	// of a ClusterTrustBundle object in an auto-updating file. | ||||
| 	// of ClusterTrustBundle objects in an auto-updating file. | ||||
| 	// | ||||
| 	// Alpha, gated by the ClusterTrustBundleProjection feature gate. | ||||
| 	// | ||||
| 	// ClusterTrustBundle objects can either be selected by name, or by the | ||||
| 	// combination of signer name and a label selector. | ||||
| 	// | ||||
| 	// When selecting by name, the referenced ClusterTrustBundle object must | ||||
| 	// have an empty spec.signerName field. | ||||
| 	// | ||||
| 	// When selecting by signer name, the contents of all ClusterTrustBundle | ||||
| 	// objects associated with the signer and matching the label will be unified | ||||
| 	// and deduplicated. | ||||
| 	// | ||||
| 	// Kubelet performs aggressive normalization of the PEM contents written | ||||
| 	// into the pod filesystem.  Esoteric PEM features such as inter-block | ||||
| 	// comments and block headers are stripped.  Certificates are deduplicated. | ||||
| 	// The ordering of certificates within the file is arbitrary, and Kubelet | ||||
| 	// may change the order over time. | ||||
| 	// | ||||
| 	// +featureGate=ClusterTrustBundleProjection | ||||
| 	// +optional | ||||
| 	ClusterTrustBundle *ClusterTrustBundleProjection `json:"clusterTrustBundle,omitempty" protobuf:"bytes,5,opt,name=clusterTrustBundle"` | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,48 @@ | ||||
| /* | ||||
| Copyright 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. | ||||
| */ | ||||
|  | ||||
| // Code generated by applyconfiguration-gen. DO NOT EDIT. | ||||
|  | ||||
| package v1 | ||||
|  | ||||
| // ClusterTrustBundlePEMProjectionApplyConfiguration represents an declarative configuration of the ClusterTrustBundlePEMProjection type for use | ||||
| // with apply. | ||||
| type ClusterTrustBundlePEMProjectionApplyConfiguration struct { | ||||
| 	Name *string `json:"name,omitempty"` | ||||
| 	Path *string `json:"path,omitempty"` | ||||
| } | ||||
|  | ||||
| // ClusterTrustBundlePEMProjectionApplyConfiguration constructs an declarative configuration of the ClusterTrustBundlePEMProjection type for use with | ||||
| // apply. | ||||
| func ClusterTrustBundlePEMProjection() *ClusterTrustBundlePEMProjectionApplyConfiguration { | ||||
| 	return &ClusterTrustBundlePEMProjectionApplyConfiguration{} | ||||
| } | ||||
|  | ||||
| // WithName sets the Name field in the declarative configuration to the given value | ||||
| // and returns the receiver, so that objects can be built by chaining "With" function invocations. | ||||
| // If called multiple times, the Name field is set to the value of the last call. | ||||
| func (b *ClusterTrustBundlePEMProjectionApplyConfiguration) WithName(value string) *ClusterTrustBundlePEMProjectionApplyConfiguration { | ||||
| 	b.Name = &value | ||||
| 	return b | ||||
| } | ||||
|  | ||||
| // WithPath sets the Path field in the declarative configuration to the given value | ||||
| // and returns the receiver, so that objects can be built by chaining "With" function invocations. | ||||
| // If called multiple times, the Path field is set to the value of the last call. | ||||
| func (b *ClusterTrustBundlePEMProjectionApplyConfiguration) WithPath(value string) *ClusterTrustBundlePEMProjectionApplyConfiguration { | ||||
| 	b.Path = &value | ||||
| 	return b | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Taahir Ahmed
					Taahir Ahmed