diff --git a/pkg/api/pod/util.go b/pkg/api/pod/util.go index 685db7a8851..eba358b1805 100644 --- a/pkg/api/pod/util.go +++ b/pkg/api/pod/util.go @@ -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 { diff --git a/pkg/apis/certificates/validation/validation.go b/pkg/apis/certificates/validation/validation.go index e277cd74898..ac9e8de3075 100644 --- a/pkg/apis/certificates/validation/validation.go +++ b/pkg/apis/certificates/validation/validation.go @@ -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: -// /.. -// 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...) } diff --git a/pkg/apis/core/types.go b/pkg/apis/core/types.go index 41305a58604..fa1242b8a1d 100644 --- a/pkg/apis/core/types.go +++ b/pkg/apis/core/types.go @@ -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. diff --git a/pkg/apis/core/validation/names.go b/pkg/apis/core/validation/names.go new file mode 100644 index 00000000000..398a1cb3a2c --- /dev/null +++ b/pkg/apis/core/validation/names.go @@ -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: +// /.. +// 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 + } +} diff --git a/pkg/apis/core/validation/validation.go b/pkg/apis/core/validation/validation.go index 30749fc3dc3..a6f7fef3012 100644 --- a/pkg/apis/core/validation/validation.go +++ b/pkg/apis/core/validation/validation.go @@ -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")) } diff --git a/pkg/apis/core/validation/validation_test.go b/pkg/apis/core/validation/validation_test.go index f8dff44fb52..9dc0a70b8fe 100644 --- a/pkg/apis/core/validation/validation_test.go +++ b/pkg/apis/core/validation/validation_test.go @@ -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{ diff --git a/plugin/pkg/admission/serviceaccount/admission.go b/plugin/pkg/admission/serviceaccount/admission.go index c844a051c24..2d7dfee6993 100644 --- a/plugin/pkg/admission/serviceaccount/admission.go +++ b/plugin/pkg/admission/serviceaccount/admission.go @@ -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")) + } } } } diff --git a/staging/src/k8s.io/api/core/v1/types.go b/staging/src/k8s.io/api/core/v1/types.go index 4ba628a377c..7e283108078 100644 --- a/staging/src/k8s.io/api/core/v1/types.go +++ b/staging/src/k8s.io/api/core/v1/types.go @@ -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"` } diff --git a/staging/src/k8s.io/client-go/applyconfigurations/core/v1/clustertrustbundlepemprojection.go b/staging/src/k8s.io/client-go/applyconfigurations/core/v1/clustertrustbundlepemprojection.go new file mode 100644 index 00000000000..d9f7d5b4012 --- /dev/null +++ b/staging/src/k8s.io/client-go/applyconfigurations/core/v1/clustertrustbundlepemprojection.go @@ -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 +}