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:
Taahir Ahmed 2022-10-21 19:50:01 -07:00
parent 0fd1362782
commit ecfdc8fda5
9 changed files with 523 additions and 114 deletions

View File

@ -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 {

View File

@ -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...)
}

View File

@ -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.

View 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
}
}

View File

@ -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"))
}

View File

@ -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{

View File

@ -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"))
}
}
}
}

View File

@ -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"`
}

View File

@ -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
}