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