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) | 	dropDisabledMatchLabelKeysFieldInTopologySpread(podSpec, oldPodSpec) | ||||||
| 	dropDisabledMatchLabelKeysFieldInPodAffinity(podSpec, oldPodSpec) | 	dropDisabledMatchLabelKeysFieldInPodAffinity(podSpec, oldPodSpec) | ||||||
| 	dropDisabledDynamicResourceAllocationFields(podSpec, oldPodSpec) | 	dropDisabledDynamicResourceAllocationFields(podSpec, oldPodSpec) | ||||||
|  | 	dropDisabledClusterTrustBundleProjection(podSpec, oldPodSpec) | ||||||
|  |  | ||||||
| 	if !utilfeature.DefaultFeatureGate.Enabled(features.InPlacePodVerticalScaling) && !inPlacePodVerticalScalingInUse(oldPodSpec) { | 	if !utilfeature.DefaultFeatureGate.Enabled(features.InPlacePodVerticalScaling) && !inPlacePodVerticalScalingInUse(oldPodSpec) { | ||||||
| 		// Drop ResizePolicy fields. Don't drop updates to Resources field as template.spec.resources | 		// 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 | 	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 { | func hasInvalidLabelValueInAffinitySelector(spec *api.PodSpec) bool { | ||||||
| 	if spec.Affinity != nil { | 	if spec.Affinity != nil { | ||||||
| 		if spec.Affinity.PodAffinity != nil { | 		if spec.Affinity.PodAffinity != nil { | ||||||
|   | |||||||
| @@ -21,14 +21,11 @@ import ( | |||||||
| 	"crypto/x509" | 	"crypto/x509" | ||||||
| 	"encoding/pem" | 	"encoding/pem" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"strings" |  | ||||||
|  |  | ||||||
| 	"github.com/google/go-cmp/cmp" | 	"github.com/google/go-cmp/cmp" | ||||||
| 	v1 "k8s.io/api/core/v1" | 	v1 "k8s.io/api/core/v1" | ||||||
| 	apiequality "k8s.io/apimachinery/pkg/api/equality" | 	apiequality "k8s.io/apimachinery/pkg/api/equality" | ||||||
| 	apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation" |  | ||||||
| 	"k8s.io/apimachinery/pkg/util/sets" | 	"k8s.io/apimachinery/pkg/util/sets" | ||||||
| 	utilvalidation "k8s.io/apimachinery/pkg/util/validation" |  | ||||||
| 	"k8s.io/apimachinery/pkg/util/validation/field" | 	"k8s.io/apimachinery/pkg/util/validation/field" | ||||||
| 	utilcert "k8s.io/client-go/util/cert" | 	utilcert "k8s.io/client-go/util/cert" | ||||||
| 	"k8s.io/kubernetes/pkg/apis/certificates" | 	"k8s.io/kubernetes/pkg/apis/certificates" | ||||||
| @@ -198,7 +195,7 @@ func validateCertificateSigningRequest(csr *certificates.CertificateSigningReque | |||||||
| 	if !opts.allowLegacySignerName && csr.Spec.SignerName == certificates.LegacyUnknownSignerName { | 	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")) | 		allErrs = append(allErrs, field.Invalid(specPath.Child("signerName"), csr.Spec.SignerName, "the legacy signerName is not allowed via this API version")) | ||||||
| 	} else { | 	} 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 { | 	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)")) | 		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 | 	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 { | func ValidateCertificateSigningRequestUpdate(newCSR, oldCSR *certificates.CertificateSigningRequest) field.ErrorList { | ||||||
| 	opts := getValidationOptions(newCSR, oldCSR) | 	opts := getValidationOptions(newCSR, oldCSR) | ||||||
| 	return validateCertificateSigningRequestUpdate(newCSR, oldCSR, opts) | 	return validateCertificateSigningRequestUpdate(newCSR, oldCSR, opts) | ||||||
| @@ -539,24 +460,6 @@ func hasDuplicateUsage(usages []certificates.KeyUsage) bool { | |||||||
| 	return false | 	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 { | type ValidateClusterTrustBundleOptions struct { | ||||||
| 	SuppressBundleParsing bool | 	SuppressBundleParsing bool | ||||||
| } | } | ||||||
| @@ -565,11 +468,11 @@ type ValidateClusterTrustBundleOptions struct { | |||||||
| func ValidateClusterTrustBundle(bundle *certificates.ClusterTrustBundle, opts ValidateClusterTrustBundleOptions) field.ErrorList { | func ValidateClusterTrustBundle(bundle *certificates.ClusterTrustBundle, opts ValidateClusterTrustBundleOptions) field.ErrorList { | ||||||
| 	var allErrors 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...) | 	allErrors = append(allErrors, metaErrors...) | ||||||
|  |  | ||||||
| 	if bundle.Spec.SignerName != "" { | 	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...) | 		allErrors = append(allErrors, signerNameErrors...) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1759,6 +1759,29 @@ type ServiceAccountTokenProjection struct { | |||||||
| 	Path string | 	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 | // ProjectedVolumeSource represents a projected volume source | ||||||
| type ProjectedVolumeSource struct { | type ProjectedVolumeSource struct { | ||||||
| 	// list of volume projections | 	// list of volume projections | ||||||
| @@ -1784,6 +1807,8 @@ type VolumeProjection struct { | |||||||
| 	ConfigMap *ConfigMapProjection | 	ConfigMap *ConfigMapProjection | ||||||
| 	// information about the serviceAccountToken data to project | 	// information about the serviceAccountToken data to project | ||||||
| 	ServiceAccountToken *ServiceAccountTokenProjection | 	ServiceAccountToken *ServiceAccountTokenProjection | ||||||
|  | 	// information about the ClusterTrustBundle data to project | ||||||
|  | 	ClusterTrustBundle *ClusterTrustBundleProjection | ||||||
| } | } | ||||||
|  |  | ||||||
| // KeyToPath maps a string key to a path within a volume. | // 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"), "")) | 				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 { | 		if numSources > 1 { | ||||||
| 			allErrs = append(allErrs, field.Forbidden(srcPath, "may not specify more than 1 volume type")) | 			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": { | 		"ephemeral volume + PVC, no conflict between them": { | ||||||
| 			ObjectMeta: metav1.ObjectMeta{Name: "123", Namespace: "ns"}, | 			ObjectMeta: metav1.ObjectMeta{Name: "123", Namespace: "ns"}, | ||||||
| 			Spec: core.PodSpec{ | 			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": { | 		"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", | 			expectedError: "spec.volumes[1].name: Invalid value: \"" + longVolName + "\": PVC name \"" + longPodName + "-" + longVolName + "\": must be no more than 253 characters", | ||||||
| 			spec: core.Pod{ | 			spec: core.Pod{ | ||||||
|   | |||||||
| @@ -210,6 +210,9 @@ 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")) | ||||||
|  | 					} | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|   | |||||||
| @@ -1842,22 +1842,31 @@ type ServiceAccountTokenProjection struct { | |||||||
| // filesystem. | // filesystem. | ||||||
| type ClusterTrustBundleProjection struct { | type ClusterTrustBundleProjection struct { | ||||||
| 	// Select a single ClusterTrustBundle by object name.  Mutually-exclusive | 	// Select a single ClusterTrustBundle by object name.  Mutually-exclusive | ||||||
| 	// with SignerName and LabelSelector. | 	// with signerName and labelSelector. | ||||||
| 	// +optional | 	// +optional | ||||||
| 	Name *string `json:"name,omitempty" protobuf:"bytes,1,rep,name=name"` | 	Name *string `json:"name,omitempty" protobuf:"bytes,1,rep,name=name"` | ||||||
|  |  | ||||||
| 	// Select all ClusterTrustBundles that match this signer 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 | 	// +optional | ||||||
| 	SignerName *string `json:"signerName,omitempty" protobuf:"bytes,2,rep,name=signerName"` | 	SignerName *string `json:"signerName,omitempty" protobuf:"bytes,2,rep,name=signerName"` | ||||||
|  |  | ||||||
| 	// Select all ClusterTrustBundles that match this label selector.  Must not | 	// Select all ClusterTrustBundles that match this label selector.  Only has | ||||||
| 	// be null or empty if SignerName is provided.  Mutually-exclusive with | 	// effect if signerName is set.  Mutually-exclusive with name.  If unset, | ||||||
| 	// Name. | 	// interpreted as "match nothing".  If set but empty, interpreted as "match | ||||||
| 	// | 	// everything". | ||||||
| 	// +optional | 	// +optional | ||||||
| 	LabelSelector *metav1.LabelSelector `json:"labelSelector,omitempty" protobuf:"bytes,3,rep,name=labelSelector"` | 	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. | 	// Relative path from the volume root to write the bundle. | ||||||
| 	Path string `json:"path" protobuf:"bytes,4,rep,name=path"` | 	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"` | 	ServiceAccountToken *ServiceAccountTokenProjection `json:"serviceAccountToken,omitempty" protobuf:"bytes,4,opt,name=serviceAccountToken"` | ||||||
|  |  | ||||||
| 	// ClusterTrustBundle allows a pod to access the `.spec.trustBundle` field | 	// 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. | 	// Alpha, gated by the ClusterTrustBundleProjection feature gate. | ||||||
| 	// | 	// | ||||||
| 	// ClusterTrustBundle objects can either be selected by name, or by the | 	// ClusterTrustBundle objects can either be selected by name, or by the | ||||||
| 	// combination of signer name and a label selector. | 	// 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 | 	// Kubelet performs aggressive normalization of the PEM contents written | ||||||
| 	// into the pod filesystem.  Esoteric PEM features such as inter-block | 	// into the pod filesystem.  Esoteric PEM features such as inter-block | ||||||
| 	// comments and block headers are stripped.  Certificates are deduplicated. | 	// comments and block headers are stripped.  Certificates are deduplicated. | ||||||
| 	// The ordering of certificates within the file is arbitrary, and Kubelet | 	// The ordering of certificates within the file is arbitrary, and Kubelet | ||||||
| 	// may change the order over time. | 	// may change the order over time. | ||||||
| 	// | 	// | ||||||
|  | 	// +featureGate=ClusterTrustBundleProjection | ||||||
| 	// +optional | 	// +optional | ||||||
| 	ClusterTrustBundle *ClusterTrustBundleProjection `json:"clusterTrustBundle,omitempty" protobuf:"bytes,5,opt,name=clusterTrustBundle"` | 	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