mirror of
				https://github.com/optim-enterprises-bv/kubernetes.git
				synced 2025-10-31 10:18:13 +00:00 
			
		
		
		
	ClusterTrustBundles: Define types
This commit is the main API piece of KEP-3257 (ClusterTrustBundles). This commit: * Adds the certificates.k8s.io/v1alpha1 API group * Adds the ClusterTrustBundle type. * Registers the new type in kube-apiserver. * Implements the type-specfic validation specified for ClusterTrustBundles: - spec.pemTrustAnchors must always be non-empty. - spec.signerName must be either empty or a valid signer name. - Changing spec.signerName is disallowed. * Implements the "attest" admission check to restrict actions on ClusterTrustBundles that include a signer name. Because it wasn't specified in the KEP, I chose to make attempts to update the signer name be validation errors, rather than silently ignored. I have tested this out by launching these changes in kind and manipulating ClusterTrustBundle objects in the resulting cluster using kubectl.
This commit is contained in:
		| @@ -261,6 +261,7 @@ var apiVersionPriorities = map[schema.GroupVersion]priority{ | |||||||
| 	{Group: "batch", Version: "v1beta1"}:                         {group: 17400, version: 9}, | 	{Group: "batch", Version: "v1beta1"}:                         {group: 17400, version: 9}, | ||||||
| 	{Group: "batch", Version: "v2alpha1"}:                        {group: 17400, version: 9}, | 	{Group: "batch", Version: "v2alpha1"}:                        {group: 17400, version: 9}, | ||||||
| 	{Group: "certificates.k8s.io", Version: "v1"}:                {group: 17300, version: 15}, | 	{Group: "certificates.k8s.io", Version: "v1"}:                {group: 17300, version: 15}, | ||||||
|  | 	{Group: "certificates.k8s.io", Version: "v1alpha1"}:          {group: 17300, version: 1}, | ||||||
| 	{Group: "networking.k8s.io", Version: "v1"}:                  {group: 17200, version: 15}, | 	{Group: "networking.k8s.io", Version: "v1"}:                  {group: 17200, version: 15}, | ||||||
| 	{Group: "networking.k8s.io", Version: "v1alpha1"}:            {group: 17200, version: 1}, | 	{Group: "networking.k8s.io", Version: "v1alpha1"}:            {group: 17200, version: 1}, | ||||||
| 	{Group: "policy", Version: "v1"}:                             {group: 17100, version: 15}, | 	{Group: "policy", Version: "v1"}:                             {group: 17100, version: 15}, | ||||||
|   | |||||||
| @@ -88,6 +88,7 @@ batch/v1 \ | |||||||
| batch/v1beta1 \ | batch/v1beta1 \ | ||||||
| certificates.k8s.io/v1 \ | certificates.k8s.io/v1 \ | ||||||
| certificates.k8s.io/v1beta1 \ | certificates.k8s.io/v1beta1 \ | ||||||
|  | certificates.k8s.io/v1alpha1 \ | ||||||
| coordination.k8s.io/v1beta1 \ | coordination.k8s.io/v1beta1 \ | ||||||
| coordination.k8s.io/v1 \ | coordination.k8s.io/v1 \ | ||||||
| discovery.k8s.io/v1 \ | discovery.k8s.io/v1 \ | ||||||
|   | |||||||
| @@ -24,6 +24,7 @@ import ( | |||||||
| 	"k8s.io/kubernetes/pkg/api/legacyscheme" | 	"k8s.io/kubernetes/pkg/api/legacyscheme" | ||||||
| 	"k8s.io/kubernetes/pkg/apis/certificates" | 	"k8s.io/kubernetes/pkg/apis/certificates" | ||||||
| 	v1 "k8s.io/kubernetes/pkg/apis/certificates/v1" | 	v1 "k8s.io/kubernetes/pkg/apis/certificates/v1" | ||||||
|  | 	"k8s.io/kubernetes/pkg/apis/certificates/v1alpha1" | ||||||
| 	"k8s.io/kubernetes/pkg/apis/certificates/v1beta1" | 	"k8s.io/kubernetes/pkg/apis/certificates/v1beta1" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -36,5 +37,6 @@ func Install(scheme *runtime.Scheme) { | |||||||
| 	utilruntime.Must(certificates.AddToScheme(scheme)) | 	utilruntime.Must(certificates.AddToScheme(scheme)) | ||||||
| 	utilruntime.Must(v1.AddToScheme(scheme)) | 	utilruntime.Must(v1.AddToScheme(scheme)) | ||||||
| 	utilruntime.Must(v1beta1.AddToScheme(scheme)) | 	utilruntime.Must(v1beta1.AddToScheme(scheme)) | ||||||
| 	utilruntime.Must(scheme.SetVersionPriority(v1.SchemeGroupVersion, v1beta1.SchemeGroupVersion)) | 	utilruntime.Must(v1alpha1.AddToScheme(scheme)) | ||||||
|  | 	utilruntime.Must(scheme.SetVersionPriority(v1.SchemeGroupVersion, v1beta1.SchemeGroupVersion, v1alpha1.SchemeGroupVersion)) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -47,6 +47,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { | |||||||
| 	scheme.AddKnownTypes(SchemeGroupVersion, | 	scheme.AddKnownTypes(SchemeGroupVersion, | ||||||
| 		&CertificateSigningRequest{}, | 		&CertificateSigningRequest{}, | ||||||
| 		&CertificateSigningRequestList{}, | 		&CertificateSigningRequestList{}, | ||||||
|  | 		&ClusterTrustBundle{}, | ||||||
|  | 		&ClusterTrustBundleList{}, | ||||||
| 	) | 	) | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|   | |||||||
| @@ -224,3 +224,56 @@ const ( | |||||||
| 	UsageMicrosoftSGC      KeyUsage = "microsoft sgc" | 	UsageMicrosoftSGC      KeyUsage = "microsoft sgc" | ||||||
| 	UsageNetscapeSGC       KeyUsage = "netscape sgc" | 	UsageNetscapeSGC       KeyUsage = "netscape sgc" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object | ||||||
|  |  | ||||||
|  | // ClusterTrustBundle is a cluster-scoped container for X.509 trust anchors | ||||||
|  | // (root certificates). | ||||||
|  | // | ||||||
|  | // ClusterTrustBundle objects are considered to be readable by any authenticated | ||||||
|  | // user in the cluster. | ||||||
|  | // | ||||||
|  | // It can be optionally associated with a particular assigner, in which case it | ||||||
|  | // contains one valid set of trust anchors for that signer. Signers may have | ||||||
|  | // multiple associated ClusterTrustBundles; each is an independent set of trust | ||||||
|  | // anchors for that signer. | ||||||
|  | type ClusterTrustBundle struct { | ||||||
|  | 	metav1.TypeMeta | ||||||
|  | 	// +optional | ||||||
|  | 	metav1.ObjectMeta | ||||||
|  |  | ||||||
|  | 	// Spec contains the signer (if any) and trust anchors. | ||||||
|  | 	// +optional | ||||||
|  | 	Spec ClusterTrustBundleSpec | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ClusterTrustBundleSpec contains the signer and trust anchors. | ||||||
|  | type ClusterTrustBundleSpec struct { | ||||||
|  | 	// SignerName indicates the associated signer, if any. | ||||||
|  | 	SignerName string | ||||||
|  |  | ||||||
|  | 	// TrustBundle contains the individual X.509 trust anchors for this | ||||||
|  | 	// bundle, as PEM bundle of PEM-wrapped, DER-formatted X.509 certificates. | ||||||
|  | 	// | ||||||
|  | 	// The data must consist only of PEM certificate blocks that parse as valid | ||||||
|  | 	// X.509 certificates.  Each certificate must include a basic constraints | ||||||
|  | 	// extension with the CA bit set.  The API server will reject objects that | ||||||
|  | 	// contain duplicate certificates, or that use PEM block headers. | ||||||
|  | 	// | ||||||
|  | 	// Users of ClusterTrustBundles, including Kubelet, are free to reorder and | ||||||
|  | 	// deduplicate certificate blocks in this file according to their own logic, | ||||||
|  | 	// as well as to drop PEM block headers and inter-block data. | ||||||
|  | 	TrustBundle string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object | ||||||
|  |  | ||||||
|  | // ClusterTrustBundleList is a collection of ClusterTrustBundle objects | ||||||
|  | type ClusterTrustBundleList struct { | ||||||
|  | 	metav1.TypeMeta | ||||||
|  | 	// +optional | ||||||
|  | 	metav1.ListMeta | ||||||
|  |  | ||||||
|  | 	// Items is a collection of ClusterTrustBundle objects | ||||||
|  | 	Items []ClusterTrustBundle | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										37
									
								
								pkg/apis/certificates/v1alpha1/conversion.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								pkg/apis/certificates/v1alpha1/conversion.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | |||||||
|  | /* | ||||||
|  | Copyright 2022 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 v1alpha1 | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  |  | ||||||
|  | 	"k8s.io/apimachinery/pkg/runtime" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func addConversionFuncs(scheme *runtime.Scheme) error { | ||||||
|  | 	return scheme.AddFieldLabelConversionFunc( | ||||||
|  | 		SchemeGroupVersion.WithKind("ClusterTrustBundle"), | ||||||
|  | 		func(label, value string) (string, string, error) { | ||||||
|  | 			switch label { | ||||||
|  | 			case "metadata.name", "spec.signerName": | ||||||
|  | 				return label, value, nil | ||||||
|  | 			default: | ||||||
|  | 				return "", "", fmt.Errorf("field label not supported: %s", label) | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | } | ||||||
							
								
								
									
										23
									
								
								pkg/apis/certificates/v1alpha1/defaults.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								pkg/apis/certificates/v1alpha1/defaults.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | /* | ||||||
|  | Copyright 2022 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 v1alpha1 | ||||||
|  |  | ||||||
|  | import "k8s.io/apimachinery/pkg/runtime" | ||||||
|  |  | ||||||
|  | func addDefaultingFuncs(scheme *runtime.Scheme) error { | ||||||
|  | 	return RegisterDefaults(scheme) | ||||||
|  | } | ||||||
							
								
								
									
										24
									
								
								pkg/apis/certificates/v1alpha1/doc.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								pkg/apis/certificates/v1alpha1/doc.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | |||||||
|  | /* | ||||||
|  | Copyright 2022 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. | ||||||
|  | */ | ||||||
|  |  | ||||||
|  | // +k8s:conversion-gen=k8s.io/kubernetes/pkg/apis/certificates | ||||||
|  | // +k8s:conversion-gen-external-types=k8s.io/api/certificates/v1alpha1 | ||||||
|  | // +k8s:defaulter-gen=TypeMeta | ||||||
|  | // +k8s:defaulter-gen-input=k8s.io/api/certificates/v1alpha1 | ||||||
|  |  | ||||||
|  | // +groupName=certificates.k8s.io | ||||||
|  |  | ||||||
|  | package v1alpha1 // import "k8s.io/kubernetes/pkg/apis/certificates/v1alpha1" | ||||||
							
								
								
									
										43
									
								
								pkg/apis/certificates/v1alpha1/register.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								pkg/apis/certificates/v1alpha1/register.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | |||||||
|  | /* | ||||||
|  | Copyright 2022 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 v1alpha1 | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	certificatesv1alpha1 "k8s.io/api/certificates/v1alpha1" | ||||||
|  | 	"k8s.io/apimachinery/pkg/runtime/schema" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // GroupName is the group name used in this package. | ||||||
|  | const GroupName = "certificates.k8s.io" | ||||||
|  |  | ||||||
|  | // SchemeGroupVersion is the group and version used in this package. | ||||||
|  | var SchemeGroupVersion = schema.GroupVersion{ | ||||||
|  | 	Group:   GroupName, | ||||||
|  | 	Version: "v1alpha1", | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	localSchemeBuilder = &certificatesv1alpha1.SchemeBuilder | ||||||
|  | 	AddToScheme        = localSchemeBuilder.AddToScheme | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func init() { | ||||||
|  | 	// We only register manually written functions here. The registration of the | ||||||
|  | 	// generated functions takes place in the generated files. The separation | ||||||
|  | 	// makes the code compile even when the generated files are missing. | ||||||
|  | 	localSchemeBuilder.Register(addDefaultingFuncs, addConversionFuncs) | ||||||
|  | } | ||||||
| @@ -25,6 +25,7 @@ import ( | |||||||
|  |  | ||||||
| 	v1 "k8s.io/api/core/v1" | 	v1 "k8s.io/api/core/v1" | ||||||
| 	apiequality "k8s.io/apimachinery/pkg/api/equality" | 	apiequality "k8s.io/apimachinery/pkg/api/equality" | ||||||
|  | 	apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation" | ||||||
| 	"k8s.io/apimachinery/pkg/util/diff" | 	"k8s.io/apimachinery/pkg/util/diff" | ||||||
| 	"k8s.io/apimachinery/pkg/util/sets" | 	"k8s.io/apimachinery/pkg/util/sets" | ||||||
| 	utilvalidation "k8s.io/apimachinery/pkg/util/validation" | 	utilvalidation "k8s.io/apimachinery/pkg/util/validation" | ||||||
| @@ -197,7 +198,7 @@ func validateCertificateSigningRequest(csr *certificates.CertificateSigningReque | |||||||
| 	if !opts.allowLegacySignerName && csr.Spec.SignerName == certificates.LegacyUnknownSignerName { | 	if !opts.allowLegacySignerName && csr.Spec.SignerName == certificates.LegacyUnknownSignerName { | ||||||
| 		allErrs = append(allErrs, field.Invalid(specPath.Child("signerName"), csr.Spec.SignerName, "the legacy signerName is not allowed via this API version")) | 		allErrs = append(allErrs, field.Invalid(specPath.Child("signerName"), csr.Spec.SignerName, "the legacy signerName is not allowed via this API version")) | ||||||
| 	} else { | 	} else { | ||||||
| 		allErrs = append(allErrs, ValidateCertificateSigningRequestSignerName(specPath.Child("signerName"), csr.Spec.SignerName)...) | 		allErrs = append(allErrs, ValidateSignerName(specPath.Child("signerName"), csr.Spec.SignerName)...) | ||||||
| 	} | 	} | ||||||
| 	if csr.Spec.ExpirationSeconds != nil && *csr.Spec.ExpirationSeconds < 600 { | 	if csr.Spec.ExpirationSeconds != nil && *csr.Spec.ExpirationSeconds < 600 { | ||||||
| 		allErrs = append(allErrs, field.Invalid(specPath.Child("expirationSeconds"), *csr.Spec.ExpirationSeconds, "may not specify a duration less than 600 seconds (10 minutes)")) | 		allErrs = append(allErrs, field.Invalid(specPath.Child("expirationSeconds"), *csr.Spec.ExpirationSeconds, "may not specify a duration less than 600 seconds (10 minutes)")) | ||||||
| @@ -272,7 +273,7 @@ func validateConditions(fldPath *field.Path, csr *certificates.CertificateSignin | |||||||
| // The max length of a namespace name is 63 characters (DNS1123Label 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) | // 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 '/'. | // We then add an additional 2 characters to account for the one '.' and one '/'. | ||||||
| func ValidateCertificateSigningRequestSignerName(fldPath *field.Path, signerName string) field.ErrorList { | func ValidateSignerName(fldPath *field.Path, signerName string) field.ErrorList { | ||||||
| 	var el field.ErrorList | 	var el field.ErrorList | ||||||
| 	if len(signerName) == 0 { | 	if len(signerName) == 0 { | ||||||
| 		el = append(el, field.Required(fldPath, "")) | 		el = append(el, field.Required(fldPath, "")) | ||||||
| @@ -537,3 +538,129 @@ func hasDuplicateUsage(usages []certificates.KeyUsage) bool { | |||||||
| 	} | 	} | ||||||
| 	return false | 	return false | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // We require your name to be prefixed by .spec.signerName | ||||||
|  | func validateClusterTrustBundleName(signerName string) func(name string, prefix bool) []string { | ||||||
|  | 	return func(name string, isPrefix bool) []string { | ||||||
|  | 		if signerName == "" { | ||||||
|  | 			if strings.Contains(name, ":") { | ||||||
|  | 				return []string{"ClusterTrustBundle without signer name must not have \":\" in its name"} | ||||||
|  | 			} | ||||||
|  | 			return apimachineryvalidation.NameIsDNSSubdomain(name, isPrefix) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		requiredPrefix := strings.ReplaceAll(signerName, "/", ":") + ":" | ||||||
|  | 		if !strings.HasPrefix(name, requiredPrefix) { | ||||||
|  | 			return []string{fmt.Sprintf("ClusterTrustBundle for signerName %s must be named with prefix %s", signerName, requiredPrefix)} | ||||||
|  | 		} | ||||||
|  | 		return apimachineryvalidation.NameIsDNSSubdomain(strings.TrimPrefix(name, requiredPrefix), isPrefix) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type ValidateClusterTrustBundleOptions struct { | ||||||
|  | 	SuppressBundleParsing bool | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ValidateClusterTrustBundle runs all validation checks on bundle. | ||||||
|  | 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")) | ||||||
|  | 	allErrors = append(allErrors, metaErrors...) | ||||||
|  |  | ||||||
|  | 	if bundle.Spec.SignerName != "" { | ||||||
|  | 		signerNameErrors := ValidateSignerName(field.NewPath("spec", "signerName"), bundle.Spec.SignerName) | ||||||
|  | 		allErrors = append(allErrors, signerNameErrors...) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if !opts.SuppressBundleParsing { | ||||||
|  | 		pemErrors := validateTrustBundle(field.NewPath("spec", "trustBundle"), bundle.Spec.TrustBundle) | ||||||
|  | 		allErrors = append(allErrors, pemErrors...) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return allErrors | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ValidateClusterTrustBundleUpdate runs all update validation checks on an | ||||||
|  | // update. | ||||||
|  | func ValidateClusterTrustBundleUpdate(newBundle, oldBundle *certificates.ClusterTrustBundle) field.ErrorList { | ||||||
|  | 	// If the caller isn't changing the TrustBundle field, don't parse it. | ||||||
|  | 	// This helps smoothly handle changes in Go's PEM or X.509 parsing | ||||||
|  | 	// libraries. | ||||||
|  | 	opts := ValidateClusterTrustBundleOptions{} | ||||||
|  | 	if newBundle.Spec.TrustBundle == oldBundle.Spec.TrustBundle { | ||||||
|  | 		opts.SuppressBundleParsing = true | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var allErrors field.ErrorList | ||||||
|  | 	allErrors = append(allErrors, ValidateClusterTrustBundle(newBundle, opts)...) | ||||||
|  | 	allErrors = append(allErrors, apivalidation.ValidateObjectMetaUpdate(&newBundle.ObjectMeta, &oldBundle.ObjectMeta, field.NewPath("metadata"))...) | ||||||
|  | 	allErrors = append(allErrors, apivalidation.ValidateImmutableField(newBundle.Spec.SignerName, oldBundle.Spec.SignerName, field.NewPath("spec", "signerName"))...) | ||||||
|  | 	return allErrors | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // validateTrustBundle rejects intra-block headers, blocks | ||||||
|  | // that don't parse as X.509 CA certificates, and duplicate trust anchors.  It | ||||||
|  | // requires that at least one trust anchor is provided. | ||||||
|  | func validateTrustBundle(path *field.Path, in string) field.ErrorList { | ||||||
|  | 	var allErrors field.ErrorList | ||||||
|  |  | ||||||
|  | 	blockDedupe := map[string][]int{} | ||||||
|  |  | ||||||
|  | 	rest := []byte(in) | ||||||
|  | 	var b *pem.Block | ||||||
|  | 	i := -1 | ||||||
|  | 	for { | ||||||
|  | 		b, rest = pem.Decode(rest) | ||||||
|  | 		if b == nil { | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 		i++ | ||||||
|  |  | ||||||
|  | 		if b.Type != "CERTIFICATE" { | ||||||
|  | 			allErrors = append(allErrors, field.Invalid(path, "<value omitted>", fmt.Sprintf("entry %d has bad block type: %v", i, b.Type))) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if len(b.Headers) != 0 { | ||||||
|  | 			allErrors = append(allErrors, field.Invalid(path, "<value omitted>", fmt.Sprintf("entry %d has PEM block headers", i))) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		cert, err := x509.ParseCertificate(b.Bytes) | ||||||
|  | 		if err != nil { | ||||||
|  | 			allErrors = append(allErrors, field.Invalid(path, "<value omitted>", fmt.Sprintf("entry %d does not parse as X.509", i))) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if !cert.IsCA { | ||||||
|  | 			allErrors = append(allErrors, field.Invalid(path, "<value omitted>", fmt.Sprintf("entry %d does not have the CA bit set", i))) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if !cert.BasicConstraintsValid { | ||||||
|  | 			allErrors = append(allErrors, field.Invalid(path, "<value omitted>", fmt.Sprintf("entry %d has invalid basic constraints", i))) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		blockDedupe[string(b.Bytes)] = append(blockDedupe[string(b.Bytes)], i) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// If we had a malformed block, don't also output potentially-redundant | ||||||
|  | 	// errors about duplicate or missing trust anchors. | ||||||
|  | 	if len(allErrors) != 0 { | ||||||
|  | 		return allErrors | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if len(blockDedupe) == 0 { | ||||||
|  | 		allErrors = append(allErrors, field.Invalid(path, "<value omitted>", "at least one trust anchor must be provided")) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, indices := range blockDedupe { | ||||||
|  | 		if len(indices) > 1 { | ||||||
|  | 			allErrors = append(allErrors, field.Invalid(path, "<value omitted>", fmt.Sprintf("duplicate trust anchor (indices %v)", indices))) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return allErrors | ||||||
|  | } | ||||||
|   | |||||||
| @@ -23,12 +23,15 @@ import ( | |||||||
| 	"crypto/x509/pkix" | 	"crypto/x509/pkix" | ||||||
| 	"encoding/pem" | 	"encoding/pem" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"math/big" | ||||||
|  | 	mathrand "math/rand" | ||||||
| 	"reflect" | 	"reflect" | ||||||
| 	"regexp" | 	"regexp" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"testing" | 	"testing" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/google/go-cmp/cmp" | ||||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
| 	"k8s.io/apimachinery/pkg/util/sets" | 	"k8s.io/apimachinery/pkg/util/sets" | ||||||
| 	"k8s.io/apimachinery/pkg/util/validation/field" | 	"k8s.io/apimachinery/pkg/util/validation/field" | ||||||
| @@ -1095,6 +1098,471 @@ func Test_validateCertificateSigningRequestOptions(t *testing.T) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func mustMakeCertificate(t *testing.T, template *x509.Certificate) []byte { | ||||||
|  | 	gen := mathrand.New(mathrand.NewSource(12345)) | ||||||
|  |  | ||||||
|  | 	pub, priv, err := ed25519.GenerateKey(gen) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Error while generating key: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	cert, err := x509.CreateCertificate(gen, template, template, pub, priv) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("Error while making certificate: %v", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return cert | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func mustMakePEMBlock(blockType string, headers map[string]string, data []byte) string { | ||||||
|  | 	return string(pem.EncodeToMemory(&pem.Block{ | ||||||
|  | 		Type:    blockType, | ||||||
|  | 		Headers: headers, | ||||||
|  | 		Bytes:   data, | ||||||
|  | 	})) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestValidateClusterTrustBundle(t *testing.T) { | ||||||
|  | 	goodCert1 := mustMakeCertificate(t, &x509.Certificate{ | ||||||
|  | 		SerialNumber: big.NewInt(0), | ||||||
|  | 		Subject: pkix.Name{ | ||||||
|  | 			CommonName: "root1", | ||||||
|  | 		}, | ||||||
|  | 		IsCA:                  true, | ||||||
|  | 		BasicConstraintsValid: true, | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	goodCert2 := mustMakeCertificate(t, &x509.Certificate{ | ||||||
|  | 		SerialNumber: big.NewInt(0), | ||||||
|  | 		Subject: pkix.Name{ | ||||||
|  | 			CommonName: "root2", | ||||||
|  | 		}, | ||||||
|  | 		IsCA:                  true, | ||||||
|  | 		BasicConstraintsValid: true, | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	badNotCACert := mustMakeCertificate(t, &x509.Certificate{ | ||||||
|  | 		SerialNumber: big.NewInt(0), | ||||||
|  | 		Subject: pkix.Name{ | ||||||
|  | 			CommonName: "root3", | ||||||
|  | 		}, | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	goodCert1Block := string(mustMakePEMBlock("CERTIFICATE", nil, goodCert1)) | ||||||
|  | 	goodCert2Block := string(mustMakePEMBlock("CERTIFICATE", nil, goodCert2)) | ||||||
|  |  | ||||||
|  | 	goodCert1AlternateBlock := strings.ReplaceAll(goodCert1Block, "\n", "\n\t\n") | ||||||
|  |  | ||||||
|  | 	badNotCACertBlock := string(mustMakePEMBlock("CERTIFICATE", nil, badNotCACert)) | ||||||
|  |  | ||||||
|  | 	badBlockHeadersBlock := string(mustMakePEMBlock("CERTIFICATE", map[string]string{"key": "value"}, goodCert1)) | ||||||
|  | 	badBlockTypeBlock := string(mustMakePEMBlock("NOTACERTIFICATE", nil, goodCert1)) | ||||||
|  | 	badNonParseableBlock := string(mustMakePEMBlock("CERTIFICATE", nil, []byte("this is not a certificate"))) | ||||||
|  |  | ||||||
|  | 	testCases := []struct { | ||||||
|  | 		description string | ||||||
|  | 		bundle      *capi.ClusterTrustBundle | ||||||
|  | 		opts        ValidateClusterTrustBundleOptions | ||||||
|  | 		wantErrors  field.ErrorList | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			description: "valid, no signer name", | ||||||
|  | 			bundle: &capi.ClusterTrustBundle{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name: "foo", | ||||||
|  | 				}, | ||||||
|  | 				Spec: capi.ClusterTrustBundleSpec{ | ||||||
|  | 					TrustBundle: goodCert1Block, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			description: "invalid, no signer name, invalid name", | ||||||
|  | 			bundle: &capi.ClusterTrustBundle{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name: "k8s.io:bar:foo", | ||||||
|  | 				}, | ||||||
|  | 				Spec: capi.ClusterTrustBundleSpec{ | ||||||
|  | 					TrustBundle: goodCert1Block, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			wantErrors: field.ErrorList{ | ||||||
|  | 				field.Invalid(field.NewPath("metadata", "name"), "k8s.io:bar:foo", "ClusterTrustBundle without signer name must not have \":\" in its name"), | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			description: "valid, with signer name", | ||||||
|  | 			bundle: &capi.ClusterTrustBundle{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name: "k8s.io:foo:bar", | ||||||
|  | 				}, | ||||||
|  | 				Spec: capi.ClusterTrustBundleSpec{ | ||||||
|  | 					SignerName:  "k8s.io/foo", | ||||||
|  | 					TrustBundle: goodCert1Block, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			description: "invalid, with signer name, missing name prefix", | ||||||
|  | 			bundle: &capi.ClusterTrustBundle{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name: "look-ma-no-prefix", | ||||||
|  | 				}, | ||||||
|  | 				Spec: capi.ClusterTrustBundleSpec{ | ||||||
|  | 					SignerName:  "k8s.io/foo", | ||||||
|  | 					TrustBundle: goodCert1Block, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			wantErrors: field.ErrorList{ | ||||||
|  | 				field.Invalid(field.NewPath("metadata", "name"), "look-ma-no-prefix", "ClusterTrustBundle for signerName k8s.io/foo must be named with prefix k8s.io:foo:"), | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			description: "invalid, with signer name, empty name suffix", | ||||||
|  | 			bundle: &capi.ClusterTrustBundle{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name: "k8s.io:foo:", | ||||||
|  | 				}, | ||||||
|  | 				Spec: capi.ClusterTrustBundleSpec{ | ||||||
|  | 					SignerName:  "k8s.io/foo", | ||||||
|  | 					TrustBundle: goodCert1Block, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			wantErrors: field.ErrorList{ | ||||||
|  | 				field.Invalid(field.NewPath("metadata", "name"), "k8s.io:foo:", `a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')`), | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			description: "invalid, with signer name, bad name suffix", | ||||||
|  | 			bundle: &capi.ClusterTrustBundle{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name: "k8s.io:foo:123notvalidDNSSubdomain", | ||||||
|  | 				}, | ||||||
|  | 				Spec: capi.ClusterTrustBundleSpec{ | ||||||
|  | 					SignerName:  "k8s.io/foo", | ||||||
|  | 					TrustBundle: goodCert1Block, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			wantErrors: field.ErrorList{ | ||||||
|  | 				field.Invalid(field.NewPath("metadata", "name"), "k8s.io:foo:123notvalidDNSSubdomain", `a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')`), | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			description: "valid, with signer name, with inter-block garbage", | ||||||
|  | 			bundle: &capi.ClusterTrustBundle{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name: "k8s.io:foo:abc", | ||||||
|  | 				}, | ||||||
|  | 				Spec: capi.ClusterTrustBundleSpec{ | ||||||
|  | 					SignerName:  "k8s.io/foo", | ||||||
|  | 					TrustBundle: "garbage\n" + goodCert1Block + "\ngarbage\n" + goodCert2Block, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			description: "invalid, no signer name, no trust anchors", | ||||||
|  | 			bundle: &capi.ClusterTrustBundle{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name: "foo", | ||||||
|  | 				}, | ||||||
|  | 				Spec: capi.ClusterTrustBundleSpec{}, | ||||||
|  | 			}, | ||||||
|  | 			wantErrors: field.ErrorList{ | ||||||
|  | 				field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "at least one trust anchor must be provided"), | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			description: "invalid, no trust anchors", | ||||||
|  | 			bundle: &capi.ClusterTrustBundle{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name: "k8s.io:foo:abc", | ||||||
|  | 				}, | ||||||
|  | 				Spec: capi.ClusterTrustBundleSpec{ | ||||||
|  | 					SignerName: "k8s.io/foo", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			wantErrors: field.ErrorList{ | ||||||
|  | 				field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "at least one trust anchor must be provided"), | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			description: "invalid, bad signer name", | ||||||
|  | 			bundle: &capi.ClusterTrustBundle{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name: "invalid:foo", | ||||||
|  | 				}, | ||||||
|  | 				Spec: capi.ClusterTrustBundleSpec{ | ||||||
|  | 					SignerName:  "invalid", | ||||||
|  | 					TrustBundle: goodCert1Block, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			wantErrors: field.ErrorList{ | ||||||
|  | 				field.Invalid(field.NewPath("spec", "signerName"), "invalid", "must be a fully qualified domain and path of the form 'example.com/signer-name'"), | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			description: "invalid, no blocks", | ||||||
|  | 			bundle: &capi.ClusterTrustBundle{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name: "foo", | ||||||
|  | 				}, | ||||||
|  | 				Spec: capi.ClusterTrustBundleSpec{ | ||||||
|  | 					TrustBundle: "non block garbage", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			wantErrors: field.ErrorList{ | ||||||
|  | 				field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "at least one trust anchor must be provided"), | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			description: "invalid, bad block type", | ||||||
|  | 			bundle: &capi.ClusterTrustBundle{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name: "foo", | ||||||
|  | 				}, | ||||||
|  | 				Spec: capi.ClusterTrustBundleSpec{ | ||||||
|  | 					TrustBundle: goodCert1Block + "\n" + badBlockTypeBlock, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			wantErrors: field.ErrorList{ | ||||||
|  | 				field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "entry 1 has bad block type: NOTACERTIFICATE"), | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			description: "invalid, block with headers", | ||||||
|  | 			bundle: &capi.ClusterTrustBundle{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name: "foo", | ||||||
|  | 				}, | ||||||
|  | 				Spec: capi.ClusterTrustBundleSpec{ | ||||||
|  | 					TrustBundle: goodCert1Block + "\n" + badBlockHeadersBlock, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			wantErrors: field.ErrorList{ | ||||||
|  | 				field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "entry 1 has PEM block headers"), | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			description: "invalid, cert is not a CA cert", | ||||||
|  | 			bundle: &capi.ClusterTrustBundle{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name: "foo", | ||||||
|  | 				}, | ||||||
|  | 				Spec: capi.ClusterTrustBundleSpec{ | ||||||
|  | 					TrustBundle: badNotCACertBlock, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			wantErrors: field.ErrorList{ | ||||||
|  | 				field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "entry 0 does not have the CA bit set"), | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			description: "invalid, duplicated blocks", | ||||||
|  | 			bundle: &capi.ClusterTrustBundle{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name: "foo", | ||||||
|  | 				}, | ||||||
|  | 				Spec: capi.ClusterTrustBundleSpec{ | ||||||
|  | 					TrustBundle: goodCert1Block + "\n" + goodCert1AlternateBlock, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			wantErrors: field.ErrorList{ | ||||||
|  | 				field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "duplicate trust anchor (indices [0 1])"), | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			description: "invalid, non-certificate entry", | ||||||
|  | 			bundle: &capi.ClusterTrustBundle{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name: "foo", | ||||||
|  | 				}, | ||||||
|  | 				Spec: capi.ClusterTrustBundleSpec{ | ||||||
|  | 					TrustBundle: goodCert1Block + "\n" + badNonParseableBlock, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			wantErrors: field.ErrorList{ | ||||||
|  | 				field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "entry 1 does not parse as X.509"), | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			description: "allow any old garbage in the PEM field if we suppress parsing", | ||||||
|  | 			bundle: &capi.ClusterTrustBundle{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name: "foo", | ||||||
|  | 				}, | ||||||
|  | 				Spec: capi.ClusterTrustBundleSpec{ | ||||||
|  | 					TrustBundle: "garbage", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			opts: ValidateClusterTrustBundleOptions{ | ||||||
|  | 				SuppressBundleParsing: true, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	for _, tc := range testCases { | ||||||
|  | 		t.Run(tc.description, func(t *testing.T) { | ||||||
|  | 			gotErrors := ValidateClusterTrustBundle(tc.bundle, tc.opts) | ||||||
|  | 			if diff := cmp.Diff(gotErrors, tc.wantErrors); diff != "" { | ||||||
|  | 				t.Fatalf("Unexpected error output from Validate; diff (-got +want)\n%s", diff) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// When there are no changes to the object, | ||||||
|  | 			// ValidateClusterTrustBundleUpdate should not report errors about | ||||||
|  | 			// the TrustBundle field. | ||||||
|  | 			tc.bundle.ObjectMeta.ResourceVersion = "1" | ||||||
|  | 			newBundle := tc.bundle.DeepCopy() | ||||||
|  | 			newBundle.ObjectMeta.ResourceVersion = "2" | ||||||
|  | 			gotErrors = ValidateClusterTrustBundleUpdate(newBundle, tc.bundle) | ||||||
|  |  | ||||||
|  | 			var filteredWantErrors field.ErrorList | ||||||
|  | 			for _, err := range tc.wantErrors { | ||||||
|  | 				if err.Field != "spec.trustBundle" { | ||||||
|  | 					filteredWantErrors = append(filteredWantErrors, err) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			if diff := cmp.Diff(gotErrors, filteredWantErrors); diff != "" { | ||||||
|  | 				t.Fatalf("Unexpected error output from ValidateUpdate; diff (-got +want)\n%s", diff) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestValidateClusterTrustBundleUpdate(t *testing.T) { | ||||||
|  | 	goodCert1 := mustMakeCertificate(t, &x509.Certificate{ | ||||||
|  | 		SerialNumber: big.NewInt(0), | ||||||
|  | 		Subject: pkix.Name{ | ||||||
|  | 			CommonName: "root1", | ||||||
|  | 		}, | ||||||
|  | 		IsCA:                  true, | ||||||
|  | 		BasicConstraintsValid: true, | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	goodCert2 := mustMakeCertificate(t, &x509.Certificate{ | ||||||
|  | 		SerialNumber: big.NewInt(0), | ||||||
|  | 		Subject: pkix.Name{ | ||||||
|  | 			CommonName: "root2", | ||||||
|  | 		}, | ||||||
|  | 		IsCA:                  true, | ||||||
|  | 		BasicConstraintsValid: true, | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	goodCert1Block := string(mustMakePEMBlock("CERTIFICATE", nil, goodCert1)) | ||||||
|  | 	goodCert2Block := string(mustMakePEMBlock("CERTIFICATE", nil, goodCert2)) | ||||||
|  |  | ||||||
|  | 	testCases := []struct { | ||||||
|  | 		description          string | ||||||
|  | 		oldBundle, newBundle *capi.ClusterTrustBundle | ||||||
|  | 		wantErrors           field.ErrorList | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			description: "changing signer name disallowed", | ||||||
|  | 			oldBundle: &capi.ClusterTrustBundle{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name: "k8s.io:foo:bar", | ||||||
|  | 				}, | ||||||
|  | 				Spec: capi.ClusterTrustBundleSpec{ | ||||||
|  | 					SignerName:  "k8s.io/foo", | ||||||
|  | 					TrustBundle: goodCert1Block, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			newBundle: &capi.ClusterTrustBundle{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name: "k8s.io:foo:bar", | ||||||
|  | 				}, | ||||||
|  | 				Spec: capi.ClusterTrustBundleSpec{ | ||||||
|  | 					SignerName:  "k8s.io/bar", | ||||||
|  | 					TrustBundle: goodCert1Block, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			wantErrors: field.ErrorList{ | ||||||
|  | 				field.Invalid(field.NewPath("metadata", "name"), "k8s.io:foo:bar", "ClusterTrustBundle for signerName k8s.io/bar must be named with prefix k8s.io:bar:"), | ||||||
|  | 				field.Invalid(field.NewPath("spec", "signerName"), "k8s.io/bar", "field is immutable"), | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			description: "adding certificate allowed", | ||||||
|  | 			oldBundle: &capi.ClusterTrustBundle{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name: "k8s.io:foo:bar", | ||||||
|  | 				}, | ||||||
|  | 				Spec: capi.ClusterTrustBundleSpec{ | ||||||
|  | 					SignerName:  "k8s.io/foo", | ||||||
|  | 					TrustBundle: goodCert1Block, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			newBundle: &capi.ClusterTrustBundle{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name: "k8s.io:foo:bar", | ||||||
|  | 				}, | ||||||
|  | 				Spec: capi.ClusterTrustBundleSpec{ | ||||||
|  | 					SignerName:  "k8s.io/foo", | ||||||
|  | 					TrustBundle: goodCert1Block + "\n" + goodCert2Block, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			description: "emptying trustBundle disallowed", | ||||||
|  | 			oldBundle: &capi.ClusterTrustBundle{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name: "k8s.io:foo:bar", | ||||||
|  | 				}, | ||||||
|  | 				Spec: capi.ClusterTrustBundleSpec{ | ||||||
|  | 					SignerName:  "k8s.io/foo", | ||||||
|  | 					TrustBundle: goodCert1Block, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			newBundle: &capi.ClusterTrustBundle{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name: "k8s.io:foo:bar", | ||||||
|  | 				}, | ||||||
|  | 				Spec: capi.ClusterTrustBundleSpec{ | ||||||
|  | 					SignerName:  "k8s.io/foo", | ||||||
|  | 					TrustBundle: "", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			wantErrors: field.ErrorList{ | ||||||
|  | 				field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "at least one trust anchor must be provided"), | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			description: "emptying trustBundle (replace with non-block garbage) disallowed", | ||||||
|  | 			oldBundle: &capi.ClusterTrustBundle{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name: "k8s.io:foo:bar", | ||||||
|  | 				}, | ||||||
|  | 				Spec: capi.ClusterTrustBundleSpec{ | ||||||
|  | 					SignerName:  "k8s.io/foo", | ||||||
|  | 					TrustBundle: goodCert1Block, | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			newBundle: &capi.ClusterTrustBundle{ | ||||||
|  | 				ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 					Name: "k8s.io:foo:bar", | ||||||
|  | 				}, | ||||||
|  | 				Spec: capi.ClusterTrustBundleSpec{ | ||||||
|  | 					SignerName:  "k8s.io/foo", | ||||||
|  | 					TrustBundle: "non block garbage", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			wantErrors: field.ErrorList{ | ||||||
|  | 				field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "at least one trust anchor must be provided"), | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, tc := range testCases { | ||||||
|  | 		t.Run(tc.description, func(t *testing.T) { | ||||||
|  | 			tc.oldBundle.ObjectMeta.ResourceVersion = "1" | ||||||
|  | 			tc.newBundle.ObjectMeta.ResourceVersion = "2" | ||||||
|  | 			gotErrors := ValidateClusterTrustBundleUpdate(tc.newBundle, tc.oldBundle) | ||||||
|  | 			if diff := cmp.Diff(gotErrors, tc.wantErrors); diff != "" { | ||||||
|  | 				t.Errorf("Unexpected error output from ValidateUpdate; diff (-got +want)\n%s", diff) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| var ( | var ( | ||||||
| 	validCertificate = []byte(` | 	validCertificate = []byte(` | ||||||
| Leading non-PEM content | Leading non-PEM content | ||||||
|   | |||||||
| @@ -40,6 +40,7 @@ import ( | |||||||
| 	batchapiv1 "k8s.io/api/batch/v1" | 	batchapiv1 "k8s.io/api/batch/v1" | ||||||
| 	batchapiv1beta1 "k8s.io/api/batch/v1beta1" | 	batchapiv1beta1 "k8s.io/api/batch/v1beta1" | ||||||
| 	certificatesapiv1 "k8s.io/api/certificates/v1" | 	certificatesapiv1 "k8s.io/api/certificates/v1" | ||||||
|  | 	certificatesv1alpha1 "k8s.io/api/certificates/v1alpha1" | ||||||
| 	coordinationapiv1 "k8s.io/api/coordination/v1" | 	coordinationapiv1 "k8s.io/api/coordination/v1" | ||||||
| 	apiv1 "k8s.io/api/core/v1" | 	apiv1 "k8s.io/api/core/v1" | ||||||
| 	discoveryv1 "k8s.io/api/discovery/v1" | 	discoveryv1 "k8s.io/api/discovery/v1" | ||||||
| @@ -734,6 +735,7 @@ var ( | |||||||
| 		apiserverinternalv1alpha1.SchemeGroupVersion, | 		apiserverinternalv1alpha1.SchemeGroupVersion, | ||||||
| 		authenticationv1alpha1.SchemeGroupVersion, | 		authenticationv1alpha1.SchemeGroupVersion, | ||||||
| 		resourcev1alpha2.SchemeGroupVersion, | 		resourcev1alpha2.SchemeGroupVersion, | ||||||
|  | 		certificatesv1alpha1.SchemeGroupVersion, | ||||||
| 		networkingapiv1alpha1.SchemeGroupVersion, | 		networkingapiv1alpha1.SchemeGroupVersion, | ||||||
| 		storageapiv1alpha1.SchemeGroupVersion, | 		storageapiv1alpha1.SchemeGroupVersion, | ||||||
| 		flowcontrolv1alpha1.SchemeGroupVersion, | 		flowcontrolv1alpha1.SchemeGroupVersion, | ||||||
|   | |||||||
| @@ -67,6 +67,12 @@ const ( | |||||||
| 	// Enables dual-stack --node-ip in kubelet with external cloud providers | 	// Enables dual-stack --node-ip in kubelet with external cloud providers | ||||||
| 	CloudDualStackNodeIPs featuregate.Feature = "CloudDualStackNodeIPs" | 	CloudDualStackNodeIPs featuregate.Feature = "CloudDualStackNodeIPs" | ||||||
|  |  | ||||||
|  | 	// owner: @ahmedtd | ||||||
|  | 	// alpha: v1.26 | ||||||
|  | 	// | ||||||
|  | 	// Enable ClusterTrustBundle objects and Kubelet integration. | ||||||
|  | 	ClusterTrustBundle featuregate.Feature = "ClusterTrustBundle" | ||||||
|  |  | ||||||
| 	// owner: @szuecs | 	// owner: @szuecs | ||||||
| 	// alpha: v1.12 | 	// alpha: v1.12 | ||||||
| 	// | 	// | ||||||
| @@ -934,6 +940,8 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS | |||||||
|  |  | ||||||
| 	CloudDualStackNodeIPs: {Default: false, PreRelease: featuregate.Alpha}, | 	CloudDualStackNodeIPs: {Default: false, PreRelease: featuregate.Alpha}, | ||||||
|  |  | ||||||
|  | 	ClusterTrustBundle: {Default: false, PreRelease: featuregate.Alpha}, | ||||||
|  |  | ||||||
| 	CPUCFSQuotaPeriod: {Default: false, PreRelease: featuregate.Alpha}, | 	CPUCFSQuotaPeriod: {Default: false, PreRelease: featuregate.Alpha}, | ||||||
|  |  | ||||||
| 	CPUManager: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // GA in 1.26 | 	CPUManager: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // GA in 1.26 | ||||||
|   | |||||||
| @@ -28,6 +28,7 @@ import ( | |||||||
| 	"k8s.io/kubernetes/pkg/api/legacyscheme" | 	"k8s.io/kubernetes/pkg/api/legacyscheme" | ||||||
| 	"k8s.io/kubernetes/pkg/apis/admissionregistration" | 	"k8s.io/kubernetes/pkg/apis/admissionregistration" | ||||||
| 	"k8s.io/kubernetes/pkg/apis/apps" | 	"k8s.io/kubernetes/pkg/apis/apps" | ||||||
|  | 	"k8s.io/kubernetes/pkg/apis/certificates" | ||||||
| 	api "k8s.io/kubernetes/pkg/apis/core" | 	api "k8s.io/kubernetes/pkg/apis/core" | ||||||
| 	"k8s.io/kubernetes/pkg/apis/events" | 	"k8s.io/kubernetes/pkg/apis/events" | ||||||
| 	"k8s.io/kubernetes/pkg/apis/extensions" | 	"k8s.io/kubernetes/pkg/apis/extensions" | ||||||
| @@ -72,6 +73,7 @@ func NewStorageFactoryConfig() *StorageFactoryConfig { | |||||||
| 		admissionregistration.Resource("validatingadmissionpolicybindings").WithVersion("v1alpha1"), | 		admissionregistration.Resource("validatingadmissionpolicybindings").WithVersion("v1alpha1"), | ||||||
| 		networking.Resource("clustercidrs").WithVersion("v1alpha1"), | 		networking.Resource("clustercidrs").WithVersion("v1alpha1"), | ||||||
| 		networking.Resource("ipaddresses").WithVersion("v1alpha1"), | 		networking.Resource("ipaddresses").WithVersion("v1alpha1"), | ||||||
|  | 		certificates.Resource("clustertrustbundles").WithVersion("v1alpha1"), | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return &StorageFactoryConfig{ | 	return &StorageFactoryConfig{ | ||||||
|   | |||||||
| @@ -26,6 +26,7 @@ import ( | |||||||
| 	"k8s.io/kubernetes/plugin/pkg/admission/alwayspullimages" | 	"k8s.io/kubernetes/plugin/pkg/admission/alwayspullimages" | ||||||
| 	"k8s.io/kubernetes/plugin/pkg/admission/antiaffinity" | 	"k8s.io/kubernetes/plugin/pkg/admission/antiaffinity" | ||||||
| 	certapproval "k8s.io/kubernetes/plugin/pkg/admission/certificates/approval" | 	certapproval "k8s.io/kubernetes/plugin/pkg/admission/certificates/approval" | ||||||
|  | 	"k8s.io/kubernetes/plugin/pkg/admission/certificates/ctbattest" | ||||||
| 	certsigning "k8s.io/kubernetes/plugin/pkg/admission/certificates/signing" | 	certsigning "k8s.io/kubernetes/plugin/pkg/admission/certificates/signing" | ||||||
| 	certsubjectrestriction "k8s.io/kubernetes/plugin/pkg/admission/certificates/subjectrestriction" | 	certsubjectrestriction "k8s.io/kubernetes/plugin/pkg/admission/certificates/subjectrestriction" | ||||||
| 	"k8s.io/kubernetes/plugin/pkg/admission/defaulttolerationseconds" | 	"k8s.io/kubernetes/plugin/pkg/admission/defaulttolerationseconds" | ||||||
| @@ -90,6 +91,7 @@ var AllOrderedPlugins = []string{ | |||||||
| 	runtimeclass.PluginName,                 // RuntimeClass | 	runtimeclass.PluginName,                 // RuntimeClass | ||||||
| 	certapproval.PluginName,                 // CertificateApproval | 	certapproval.PluginName,                 // CertificateApproval | ||||||
| 	certsigning.PluginName,                  // CertificateSigning | 	certsigning.PluginName,                  // CertificateSigning | ||||||
|  | 	ctbattest.PluginName,                    // ClusterTrustBundleAttest | ||||||
| 	certsubjectrestriction.PluginName,       // CertificateSubjectRestriction | 	certsubjectrestriction.PluginName,       // CertificateSubjectRestriction | ||||||
| 	defaultingressclass.PluginName,          // DefaultIngressClass | 	defaultingressclass.PluginName,          // DefaultIngressClass | ||||||
| 	denyserviceexternalips.PluginName,       // DenyServiceExternalIPs | 	denyserviceexternalips.PluginName,       // DenyServiceExternalIPs | ||||||
| @@ -137,6 +139,7 @@ func RegisterAllAdmissionPlugins(plugins *admission.Plugins) { | |||||||
| 	storageobjectinuseprotection.Register(plugins) | 	storageobjectinuseprotection.Register(plugins) | ||||||
| 	certapproval.Register(plugins) | 	certapproval.Register(plugins) | ||||||
| 	certsigning.Register(plugins) | 	certsigning.Register(plugins) | ||||||
|  | 	ctbattest.Register(plugins) | ||||||
| 	certsubjectrestriction.Register(plugins) | 	certsubjectrestriction.Register(plugins) | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -158,6 +161,7 @@ func DefaultOffAdmissionPlugins() sets.String { | |||||||
| 		runtimeclass.PluginName,                 // RuntimeClass | 		runtimeclass.PluginName,                 // RuntimeClass | ||||||
| 		certapproval.PluginName,                 // CertificateApproval | 		certapproval.PluginName,                 // CertificateApproval | ||||||
| 		certsigning.PluginName,                  // CertificateSigning | 		certsigning.PluginName,                  // CertificateSigning | ||||||
|  | 		ctbattest.PluginName,                    // ClusterTrustBundleAttest | ||||||
| 		certsubjectrestriction.PluginName,       // CertificateSubjectRestriction | 		certsubjectrestriction.PluginName,       // CertificateSubjectRestriction | ||||||
| 		defaultingressclass.PluginName,          // DefaultIngressClass | 		defaultingressclass.PluginName,          // DefaultIngressClass | ||||||
| 		podsecurity.PluginName,                  // PodSecurity | 		podsecurity.PluginName,                  // PodSecurity | ||||||
|   | |||||||
| @@ -31,6 +31,7 @@ import ( | |||||||
| 	autoscalingv2beta1 "k8s.io/api/autoscaling/v2beta1" | 	autoscalingv2beta1 "k8s.io/api/autoscaling/v2beta1" | ||||||
| 	batchv1 "k8s.io/api/batch/v1" | 	batchv1 "k8s.io/api/batch/v1" | ||||||
| 	batchv1beta1 "k8s.io/api/batch/v1beta1" | 	batchv1beta1 "k8s.io/api/batch/v1beta1" | ||||||
|  | 	certificatesv1alpha1 "k8s.io/api/certificates/v1alpha1" | ||||||
| 	certificatesv1beta1 "k8s.io/api/certificates/v1beta1" | 	certificatesv1beta1 "k8s.io/api/certificates/v1beta1" | ||||||
| 	coordinationv1 "k8s.io/api/coordination/v1" | 	coordinationv1 "k8s.io/api/coordination/v1" | ||||||
| 	apiv1 "k8s.io/api/core/v1" | 	apiv1 "k8s.io/api/core/v1" | ||||||
| @@ -407,6 +408,13 @@ func AddHandlers(h printers.PrintHandler) { | |||||||
| 	_ = h.TableHandler(certificateSigningRequestColumnDefinitions, printCertificateSigningRequest) | 	_ = h.TableHandler(certificateSigningRequestColumnDefinitions, printCertificateSigningRequest) | ||||||
| 	_ = h.TableHandler(certificateSigningRequestColumnDefinitions, printCertificateSigningRequestList) | 	_ = h.TableHandler(certificateSigningRequestColumnDefinitions, printCertificateSigningRequestList) | ||||||
|  |  | ||||||
|  | 	clusterTrustBundleColumnDefinitions := []metav1.TableColumnDefinition{ | ||||||
|  | 		{Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]}, | ||||||
|  | 		{Name: "SignerName", Type: "string", Description: certificatesv1alpha1.ClusterTrustBundleSpec{}.SwaggerDoc()["signerName"]}, | ||||||
|  | 	} | ||||||
|  | 	h.TableHandler(clusterTrustBundleColumnDefinitions, printClusterTrustBundle) | ||||||
|  | 	h.TableHandler(clusterTrustBundleColumnDefinitions, printClusterTrustBundleList) | ||||||
|  |  | ||||||
| 	leaseColumnDefinitions := []metav1.TableColumnDefinition{ | 	leaseColumnDefinitions := []metav1.TableColumnDefinition{ | ||||||
| 		{Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]}, | 		{Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]}, | ||||||
| 		{Name: "Holder", Type: "string", Description: coordinationv1.LeaseSpec{}.SwaggerDoc()["holderIdentity"]}, | 		{Name: "Holder", Type: "string", Description: coordinationv1.LeaseSpec{}.SwaggerDoc()["holderIdentity"]}, | ||||||
| @@ -2095,6 +2103,30 @@ func printCertificateSigningRequestList(list *certificates.CertificateSigningReq | |||||||
| 	return rows, nil | 	return rows, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func printClusterTrustBundle(obj *certificates.ClusterTrustBundle, options printers.GenerateOptions) ([]metav1.TableRow, error) { | ||||||
|  | 	row := metav1.TableRow{ | ||||||
|  | 		Object: runtime.RawExtension{Object: obj}, | ||||||
|  | 	} | ||||||
|  | 	signerName := "<none>" | ||||||
|  | 	if obj.Spec.SignerName != "" { | ||||||
|  | 		signerName = obj.Spec.SignerName | ||||||
|  | 	} | ||||||
|  | 	row.Cells = append(row.Cells, obj.Name, signerName) | ||||||
|  | 	return []metav1.TableRow{row}, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func printClusterTrustBundleList(list *certificates.ClusterTrustBundleList, options printers.GenerateOptions) ([]metav1.TableRow, error) { | ||||||
|  | 	rows := make([]metav1.TableRow, 0, len(list.Items)) | ||||||
|  | 	for i := range list.Items { | ||||||
|  | 		r, err := printClusterTrustBundle(&list.Items[i], options) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 		rows = append(rows, r...) | ||||||
|  | 	} | ||||||
|  | 	return rows, nil | ||||||
|  | } | ||||||
|  |  | ||||||
| func printComponentStatus(obj *api.ComponentStatus, options printers.GenerateOptions) ([]metav1.TableRow, error) { | func printComponentStatus(obj *api.ComponentStatus, options printers.GenerateOptions) ([]metav1.TableRow, error) { | ||||||
| 	row := metav1.TableRow{ | 	row := metav1.TableRow{ | ||||||
| 		Object: runtime.RawExtension{Object: obj}, | 		Object: runtime.RawExtension{Object: obj}, | ||||||
|   | |||||||
| @@ -0,0 +1,79 @@ | |||||||
|  | /* | ||||||
|  | Copyright 2022 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 storage | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  |  | ||||||
|  | 	"k8s.io/apimachinery/pkg/fields" | ||||||
|  | 	"k8s.io/apimachinery/pkg/labels" | ||||||
|  | 	"k8s.io/apimachinery/pkg/runtime" | ||||||
|  | 	"k8s.io/apiserver/pkg/registry/generic" | ||||||
|  | 	genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" | ||||||
|  | 	"k8s.io/apiserver/pkg/registry/rest" | ||||||
|  | 	api "k8s.io/kubernetes/pkg/apis/certificates" | ||||||
|  | 	"k8s.io/kubernetes/pkg/printers" | ||||||
|  | 	printersinternal "k8s.io/kubernetes/pkg/printers/internalversion" | ||||||
|  | 	printerstorage "k8s.io/kubernetes/pkg/printers/storage" | ||||||
|  | 	"k8s.io/kubernetes/pkg/registry/certificates/clustertrustbundle" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // REST is a RESTStorage for ClusterTrustBundle. | ||||||
|  | type REST struct { | ||||||
|  | 	*genericregistry.Store | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var _ rest.StandardStorage = &REST{} | ||||||
|  | var _ rest.TableConvertor = &REST{} | ||||||
|  | var _ genericregistry.GenericStore = &REST{} | ||||||
|  |  | ||||||
|  | // NewREST returns a RESTStorage object for ClusterTrustBundle objects. | ||||||
|  | func NewREST(optsGetter generic.RESTOptionsGetter) (*REST, error) { | ||||||
|  | 	store := &genericregistry.Store{ | ||||||
|  | 		NewFunc:                   func() runtime.Object { return &api.ClusterTrustBundle{} }, | ||||||
|  | 		NewListFunc:               func() runtime.Object { return &api.ClusterTrustBundleList{} }, | ||||||
|  | 		DefaultQualifiedResource:  api.Resource("clustertrustbundles"), | ||||||
|  | 		SingularQualifiedResource: api.Resource("clustertrustbundle"), | ||||||
|  |  | ||||||
|  | 		CreateStrategy: clustertrustbundle.Strategy, | ||||||
|  | 		UpdateStrategy: clustertrustbundle.Strategy, | ||||||
|  | 		DeleteStrategy: clustertrustbundle.Strategy, | ||||||
|  |  | ||||||
|  | 		TableConvertor: printerstorage.TableConvertor{TableGenerator: printers.NewTableGenerator().With(printersinternal.AddHandlers)}, | ||||||
|  | 	} | ||||||
|  | 	options := &generic.StoreOptions{ | ||||||
|  | 		RESTOptions: optsGetter, | ||||||
|  | 		AttrFunc:    getAttrs, | ||||||
|  | 	} | ||||||
|  | 	if err := store.CompleteWithOptions(options); err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return &REST{store}, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func getAttrs(obj runtime.Object) (labels.Set, fields.Set, error) { | ||||||
|  | 	bundle, ok := obj.(*api.ClusterTrustBundle) | ||||||
|  | 	if !ok { | ||||||
|  | 		return nil, nil, fmt.Errorf("not a clustertrustbundle") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	selectableFields := generic.MergeFieldsSets(generic.ObjectMetaFieldsSet(&bundle.ObjectMeta, false), fields.Set{ | ||||||
|  | 		"spec.signerName": bundle.Spec.SignerName, | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	return labels.Set(bundle.Labels), selectableFields, nil | ||||||
|  | } | ||||||
| @@ -0,0 +1,250 @@ | |||||||
|  | /* | ||||||
|  | Copyright 2022 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 storage | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"strings" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
|  | 	"k8s.io/apimachinery/pkg/fields" | ||||||
|  | 	"k8s.io/apimachinery/pkg/labels" | ||||||
|  | 	"k8s.io/apimachinery/pkg/runtime" | ||||||
|  | 	"k8s.io/apiserver/pkg/registry/generic" | ||||||
|  | 	genericregistrytest "k8s.io/apiserver/pkg/registry/generic/testing" | ||||||
|  | 	etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing" | ||||||
|  | 	"k8s.io/kubernetes/pkg/apis/certificates" | ||||||
|  | 	"k8s.io/kubernetes/pkg/registry/registrytest" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | const validCert1 = ` | ||||||
|  | -----BEGIN CERTIFICATE----- | ||||||
|  | MIIDmTCCAoGgAwIBAgIUUW9bIIsHU61w3yQR6amBuVvRFvcwDQYJKoZIhvcNAQEL | ||||||
|  | BQAwXDELMAkGA1UEBhMCeHgxCjAIBgNVBAgMAXgxCjAIBgNVBAcMAXgxCjAIBgNV | ||||||
|  | BAoMAXgxCjAIBgNVBAsMAXgxCzAJBgNVBAMMAmNhMRAwDgYJKoZIhvcNAQkBFgF4 | ||||||
|  | MB4XDTIyMTAxODIzNTIyNFoXDTIzMTAxODIzNTIyNFowXDELMAkGA1UEBhMCeHgx | ||||||
|  | CjAIBgNVBAgMAXgxCjAIBgNVBAcMAXgxCjAIBgNVBAoMAXgxCjAIBgNVBAsMAXgx | ||||||
|  | CzAJBgNVBAMMAmNhMRAwDgYJKoZIhvcNAQkBFgF4MIIBIjANBgkqhkiG9w0BAQEF | ||||||
|  | AAOCAQ8AMIIBCgKCAQEA4PeK4SmlsNwpw97gTtjODQytUfyqhBIwdENwJUbc019Y | ||||||
|  | m3VTCRLCGXjUa22mV6/j7V+mZw114ePFYTiGAH+2dUzWAZOphvtzE5ttPuv6A6Zx | ||||||
|  | k2J69lNFwJ2fPd7XQIH7pEIXjiEBaszxKZKMsN9+jOGu6iFFAwYLMemFYDbZHuqb | ||||||
|  | OwdQcSEsy5wO2ANzFRuYzGXuNcS8jYLHftE8g2P+L0wXnV9eW6/lM2ZFxS/nzDJz | ||||||
|  | qtzrEvQrBsmskTNC8gCRRZ7askp3CVdPKjC90sxAPwhpi8JjJZxSe1Bn/WRHUz82 | ||||||
|  | GFytEIJNx9hJY2GI316zkxgTbsxfRQe4QLJN7sRtpwIDAQABo1MwUTAdBgNVHQ4E | ||||||
|  | FgQU9FGsI8t+cu68fGkhtvO9FtUd174wHwYDVR0jBBgwFoAU9FGsI8t+cu68fGkh | ||||||
|  | tvO9FtUd174wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAqDIp | ||||||
|  | In5h2xZfEZcijT3mjfG8Bo6taxM2biy1M7wEpmDrElmrjMLsflZepcjgkSoVz9hP | ||||||
|  | cSX/k9ls1zy1H799gcjs+afSpIa1N0nUIxAKF1RHsFa+dvXpSA8YdhUnbEcBnqx0 | ||||||
|  | vN2nDBFpdCSNf+EXNEj12+9ZJm6TLzx22f9vHyRCg4D36X3Rj1FCBWxhf0mSt3ek | ||||||
|  | 5px3H53Xu42MqzZCiJc8/m+IqZHaixZS4bsayssaxif2fNxzAIZhgTygo8P8QGjI | ||||||
|  | rUmstMbg4PPq62x1yLAxEo+8XCg05saWZs384JE+K1SDqxobm51EROWVwi8jUrNC | ||||||
|  | 9nojtkQ+jDZD+1Stiw== | ||||||
|  | -----END CERTIFICATE----- | ||||||
|  | ` | ||||||
|  |  | ||||||
|  | const validCert2 = ` | ||||||
|  | -----BEGIN CERTIFICATE----- | ||||||
|  | MIIC/jCCAeagAwIBAgIBADANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwprdWJl | ||||||
|  | cm5ldGVzMB4XDTIyMTAxOTIzMTY0MFoXDTMyMTAxNjIzMTY0MFowFTETMBEGA1UE | ||||||
|  | AxMKa3ViZXJuZXRlczCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAO+k | ||||||
|  | zbj35jHIjCd5mxP1FHMwMtvLFPeKUjtaLDP9Bs2jZ97Igmr7NTysn9QZkRP68/XX | ||||||
|  | j993Y8tOLg71N4vRggWiYP+T9Xfo0uHZJmzADKx5XkuC4Gqv79dUdb8IKfAbX9HB | ||||||
|  | ffGmWRnZLLTu8Bv/vfyl0CfE64a57DK+CzNJDwdK46CYYUnEH6Wb9finYrMQ+PLG | ||||||
|  | Oi2c0J4KAYc1WTId5npNwouzf/IMD33PvuXfE7r+/pDbP8u/X03e7U0cc9l7KRxr | ||||||
|  | 3gpRQemCG74yRuy1dd3lJ1YCD8q96xVVZimGebnJ0IHi+lORRa2ix/o3OzW3FaP+ | ||||||
|  | 6kzHU6VnBRDr2rAhMh0CAwEAAaNZMFcwDgYDVR0PAQH/BAQDAgKkMA8GA1UdEwEB | ||||||
|  | /wQFMAMBAf8wHQYDVR0OBBYEFGUVOLM74t1TVoZjifsLl3Rwt1A6MBUGA1UdEQQO | ||||||
|  | MAyCCmt1YmVybmV0ZXMwDQYJKoZIhvcNAQELBQADggEBANHnPVDemZqRybYPN1as | ||||||
|  | Ywxi3iT1I3Wma1rZyxTWeIq8Ik0gnyvbtCD1cFB/5QU1xPW09YnmIFM/E73RIeWT | ||||||
|  | RmCNMgOGmegYxBQRe4UvmwWGJzKNA66c0MBmd2LDHrQlrvdewOCR667Sm9krsGt1 | ||||||
|  | tS/t6N/uBXeRSkXKEDXa+jOpYrV3Oq3IntG6zUeCrVbrH2Bs9Ma5fU00TwK3ylw5 | ||||||
|  | Ww8KzYdQaxxrLaiRRtFcpM9dFH/vwxl1QUa5vjHcmUjxmZunEmXKplATyLT0FXDw | ||||||
|  | JAo8AuwuuwRh2o+o8SxwzzA+/EBrIREgcv5uIkD352QnfGkEvGu6JOPGZVyd/kVg | ||||||
|  | KA0= | ||||||
|  | -----END CERTIFICATE----- | ||||||
|  | ` | ||||||
|  |  | ||||||
|  | func newStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) { | ||||||
|  | 	etcdStorage, server := registrytest.NewEtcdStorageForResource(t, certificates.SchemeGroupVersion.WithResource("clustertrustbundles").GroupResource()) | ||||||
|  | 	restOptions := generic.RESTOptions{ | ||||||
|  | 		StorageConfig:           etcdStorage, | ||||||
|  | 		Decorator:               generic.UndecoratedStorage, | ||||||
|  | 		DeleteCollectionWorkers: 1, | ||||||
|  | 		ResourcePrefix:          "clustertrustbundles", | ||||||
|  | 	} | ||||||
|  | 	storage, err := NewREST(restOptions) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("unexpected error from REST storage: %v", err) | ||||||
|  | 	} | ||||||
|  | 	return storage, server | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestCreate(t *testing.T) { | ||||||
|  | 	storage, server := newStorage(t) | ||||||
|  | 	defer server.Terminate(t) | ||||||
|  | 	defer storage.Store.DestroyFunc() | ||||||
|  |  | ||||||
|  | 	validBundle := &certificates.ClusterTrustBundle{ | ||||||
|  | 		ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 			Name: "ctb1", | ||||||
|  | 		}, | ||||||
|  | 		Spec: certificates.ClusterTrustBundleSpec{ | ||||||
|  | 			TrustBundle: validCert1, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	invalidBundle := &certificates.ClusterTrustBundle{ | ||||||
|  | 		ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 			Name: "ctb1", | ||||||
|  | 		}, | ||||||
|  | 		Spec: certificates.ClusterTrustBundleSpec{ | ||||||
|  | 			// Empty TrustBundle is invalid. | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	test := genericregistrytest.New(t, storage.Store) | ||||||
|  | 	test = test.ClusterScope() | ||||||
|  |  | ||||||
|  | 	test.TestCreate(validBundle, invalidBundle) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestUpdate(t *testing.T) { | ||||||
|  | 	storage, server := newStorage(t) | ||||||
|  | 	defer server.Terminate(t) | ||||||
|  | 	defer storage.Store.DestroyFunc() | ||||||
|  |  | ||||||
|  | 	test := genericregistrytest.New(t, storage.Store) | ||||||
|  | 	test = test.ClusterScope() | ||||||
|  |  | ||||||
|  | 	test.TestUpdate( | ||||||
|  | 		&certificates.ClusterTrustBundle{ | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name: "ctb1", | ||||||
|  | 			}, | ||||||
|  | 			Spec: certificates.ClusterTrustBundleSpec{ | ||||||
|  | 				TrustBundle: validCert1, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		// Valid update | ||||||
|  | 		func(object runtime.Object) runtime.Object { | ||||||
|  | 			bundle := object.(*certificates.ClusterTrustBundle) | ||||||
|  | 			bundle.Spec.TrustBundle = strings.Join([]string{validCert1, validCert2}, "\n") | ||||||
|  | 			return bundle | ||||||
|  | 		}, | ||||||
|  | 		// Invalid update | ||||||
|  | 		func(object runtime.Object) runtime.Object { | ||||||
|  | 			bundle := object.(*certificates.ClusterTrustBundle) | ||||||
|  | 			bundle.Spec.TrustBundle = "" | ||||||
|  | 			return bundle | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestDelete(t *testing.T) { | ||||||
|  | 	storage, server := newStorage(t) | ||||||
|  | 	defer server.Terminate(t) | ||||||
|  | 	defer storage.Store.DestroyFunc() | ||||||
|  |  | ||||||
|  | 	test := genericregistrytest.New(t, storage.Store) | ||||||
|  | 	test = test.ClusterScope() | ||||||
|  |  | ||||||
|  | 	test.TestDelete( | ||||||
|  | 		&certificates.ClusterTrustBundle{ | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name: "ctb1", | ||||||
|  | 			}, | ||||||
|  | 			Spec: certificates.ClusterTrustBundleSpec{ | ||||||
|  | 				TrustBundle: validCert1, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestGet(t *testing.T) { | ||||||
|  | 	storage, server := newStorage(t) | ||||||
|  | 	defer server.Terminate(t) | ||||||
|  | 	defer storage.Store.DestroyFunc() | ||||||
|  |  | ||||||
|  | 	test := genericregistrytest.New(t, storage.Store) | ||||||
|  | 	test = test.ClusterScope() | ||||||
|  |  | ||||||
|  | 	test.TestGet( | ||||||
|  | 		&certificates.ClusterTrustBundle{ | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name: "ctb1", | ||||||
|  | 			}, | ||||||
|  | 			Spec: certificates.ClusterTrustBundleSpec{ | ||||||
|  | 				TrustBundle: validCert1, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestList(t *testing.T) { | ||||||
|  | 	storage, server := newStorage(t) | ||||||
|  | 	defer server.Terminate(t) | ||||||
|  | 	defer storage.Store.DestroyFunc() | ||||||
|  |  | ||||||
|  | 	test := genericregistrytest.New(t, storage.Store) | ||||||
|  | 	test = test.ClusterScope() | ||||||
|  |  | ||||||
|  | 	test.TestList( | ||||||
|  | 		&certificates.ClusterTrustBundle{ | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name: "ctb1", | ||||||
|  | 			}, | ||||||
|  | 			Spec: certificates.ClusterTrustBundleSpec{ | ||||||
|  | 				TrustBundle: validCert1, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestWatch(t *testing.T) { | ||||||
|  | 	storage, server := newStorage(t) | ||||||
|  | 	defer server.Terminate(t) | ||||||
|  | 	defer storage.Store.DestroyFunc() | ||||||
|  |  | ||||||
|  | 	test := genericregistrytest.New(t, storage.Store) | ||||||
|  | 	test = test.ClusterScope() | ||||||
|  |  | ||||||
|  | 	test.TestWatch( | ||||||
|  | 		&certificates.ClusterTrustBundle{ | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{ | ||||||
|  | 				Name: "ctb1", | ||||||
|  | 			}, | ||||||
|  | 			Spec: certificates.ClusterTrustBundleSpec{ | ||||||
|  | 				SignerName:  "k8s.io/foo", | ||||||
|  | 				TrustBundle: validCert1, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 		// matching labels | ||||||
|  | 		[]labels.Set{}, | ||||||
|  | 		// not matching labels | ||||||
|  | 		[]labels.Set{ | ||||||
|  | 			{"foo": "bar"}, | ||||||
|  | 		}, | ||||||
|  | 		// matching fields | ||||||
|  | 		[]fields.Set{ | ||||||
|  | 			{"metadata.name": "ctb1"}, | ||||||
|  | 		}, | ||||||
|  | 		// not matching fields | ||||||
|  | 		[]fields.Set{ | ||||||
|  | 			{"metadata.name": "bar"}, | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | } | ||||||
							
								
								
									
										81
									
								
								pkg/registry/certificates/clustertrustbundle/strategy.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								pkg/registry/certificates/clustertrustbundle/strategy.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | |||||||
|  | /* | ||||||
|  | Copyright 2022 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 clustertrustbundle provides Registry interface and its RESTStorage | ||||||
|  | // implementation for storing ClusterTrustBundle objects. | ||||||
|  | package clustertrustbundle // import "k8s.io/kubernetes/pkg/registry/certificates/clustertrustbundle" | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  |  | ||||||
|  | 	"k8s.io/apimachinery/pkg/runtime" | ||||||
|  | 	"k8s.io/apimachinery/pkg/util/validation/field" | ||||||
|  | 	"k8s.io/apiserver/pkg/registry/rest" | ||||||
|  | 	"k8s.io/apiserver/pkg/storage/names" | ||||||
|  | 	"k8s.io/kubernetes/pkg/api/legacyscheme" | ||||||
|  | 	"k8s.io/kubernetes/pkg/apis/certificates" | ||||||
|  | 	certvalidation "k8s.io/kubernetes/pkg/apis/certificates/validation" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // strategy implements behavior for ClusterTrustBundles. | ||||||
|  | type strategy struct { | ||||||
|  | 	runtime.ObjectTyper | ||||||
|  | 	names.NameGenerator | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Strategy is the create, update, and delete strategy for ClusterTrustBundles. | ||||||
|  | var Strategy = strategy{legacyscheme.Scheme, names.SimpleNameGenerator} | ||||||
|  |  | ||||||
|  | var _ rest.RESTCreateStrategy = Strategy | ||||||
|  | var _ rest.RESTUpdateStrategy = Strategy | ||||||
|  | var _ rest.RESTDeleteStrategy = Strategy | ||||||
|  |  | ||||||
|  | func (strategy) NamespaceScoped() bool { | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (strategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {} | ||||||
|  |  | ||||||
|  | func (strategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { | ||||||
|  | 	bundle := obj.(*certificates.ClusterTrustBundle) | ||||||
|  | 	return certvalidation.ValidateClusterTrustBundle(bundle, certvalidation.ValidateClusterTrustBundleOptions{}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (strategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string { | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (strategy) Canonicalize(obj runtime.Object) {} | ||||||
|  |  | ||||||
|  | func (strategy) AllowCreateOnUpdate() bool { | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (s strategy) PrepareForUpdate(ctx context.Context, new, old runtime.Object) {} | ||||||
|  |  | ||||||
|  | func (s strategy) ValidateUpdate(ctx context.Context, new, old runtime.Object) field.ErrorList { | ||||||
|  | 	newBundle := new.(*certificates.ClusterTrustBundle) | ||||||
|  | 	oldBundle := old.(*certificates.ClusterTrustBundle) | ||||||
|  | 	return certvalidation.ValidateClusterTrustBundleUpdate(newBundle, oldBundle) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (strategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string { | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (strategy) AllowUnconditionalUpdate() bool { | ||||||
|  | 	return false | ||||||
|  | } | ||||||
| @@ -0,0 +1,48 @@ | |||||||
|  | /* | ||||||
|  | Copyright 2022 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 clustertrustbundle | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"k8s.io/kubernetes/pkg/apis/certificates" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestWarningsOnCreate(t *testing.T) { | ||||||
|  | 	if warnings := Strategy.WarningsOnCreate(context.Background(), &certificates.ClusterTrustBundle{}); warnings != nil { | ||||||
|  | 		t.Errorf("Got %v, want nil", warnings) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestAllowCreateOnUpdate(t *testing.T) { | ||||||
|  | 	if Strategy.AllowCreateOnUpdate() != false { | ||||||
|  | 		t.Errorf("Got true, want false") | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestWarningsOnUpdate(t *testing.T) { | ||||||
|  | 	if warnings := Strategy.WarningsOnUpdate(context.Background(), &certificates.ClusterTrustBundle{}, &certificates.ClusterTrustBundle{}); warnings != nil { | ||||||
|  | 		t.Errorf("Got %v, want nil", warnings) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestAllowUnconditionalUpdate(t *testing.T) { | ||||||
|  | 	if Strategy.AllowUnconditionalUpdate() != false { | ||||||
|  | 		t.Errorf("Got true, want false") | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -18,13 +18,18 @@ package rest | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	certificatesapiv1 "k8s.io/api/certificates/v1" | 	certificatesapiv1 "k8s.io/api/certificates/v1" | ||||||
|  | 	certificatesapiv1alpha1 "k8s.io/api/certificates/v1alpha1" | ||||||
| 	"k8s.io/apiserver/pkg/registry/generic" | 	"k8s.io/apiserver/pkg/registry/generic" | ||||||
| 	"k8s.io/apiserver/pkg/registry/rest" | 	"k8s.io/apiserver/pkg/registry/rest" | ||||||
| 	genericapiserver "k8s.io/apiserver/pkg/server" | 	genericapiserver "k8s.io/apiserver/pkg/server" | ||||||
| 	serverstorage "k8s.io/apiserver/pkg/server/storage" | 	serverstorage "k8s.io/apiserver/pkg/server/storage" | ||||||
|  | 	utilfeature "k8s.io/apiserver/pkg/util/feature" | ||||||
|  | 	"k8s.io/klog/v2" | ||||||
| 	"k8s.io/kubernetes/pkg/api/legacyscheme" | 	"k8s.io/kubernetes/pkg/api/legacyscheme" | ||||||
| 	"k8s.io/kubernetes/pkg/apis/certificates" | 	"k8s.io/kubernetes/pkg/apis/certificates" | ||||||
|  | 	"k8s.io/kubernetes/pkg/features" | ||||||
| 	certificatestore "k8s.io/kubernetes/pkg/registry/certificates/certificates/storage" | 	certificatestore "k8s.io/kubernetes/pkg/registry/certificates/certificates/storage" | ||||||
|  | 	clustertrustbundlestore "k8s.io/kubernetes/pkg/registry/certificates/clustertrustbundle/storage" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type RESTStorageProvider struct{} | type RESTStorageProvider struct{} | ||||||
| @@ -40,17 +45,22 @@ func (p RESTStorageProvider) NewRESTStorage(apiResourceConfigSource serverstorag | |||||||
| 		apiGroupInfo.VersionedResourcesStorageMap[certificatesapiv1.SchemeGroupVersion.Version] = storageMap | 		apiGroupInfo.VersionedResourcesStorageMap[certificatesapiv1.SchemeGroupVersion.Version] = storageMap | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if storageMap, err := p.v1alpha1Storage(apiResourceConfigSource, restOptionsGetter); err != nil { | ||||||
|  | 		return genericapiserver.APIGroupInfo{}, err | ||||||
|  | 	} else if len(storageMap) > 0 { | ||||||
|  | 		apiGroupInfo.VersionedResourcesStorageMap[certificatesapiv1alpha1.SchemeGroupVersion.Version] = storageMap | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	return apiGroupInfo, nil | 	return apiGroupInfo, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (p RESTStorageProvider) v1Storage(apiResourceConfigSource serverstorage.APIResourceConfigSource, restOptionsGetter generic.RESTOptionsGetter) (map[string]rest.Storage, error) { | func (p RESTStorageProvider) v1Storage(apiResourceConfigSource serverstorage.APIResourceConfigSource, restOptionsGetter generic.RESTOptionsGetter) (map[string]rest.Storage, error) { | ||||||
| 	storage := map[string]rest.Storage{} | 	storage := map[string]rest.Storage{} | ||||||
|  |  | ||||||
| 	// certificatesigningrequests |  | ||||||
| 	if resource := "certificatesigningrequests"; apiResourceConfigSource.ResourceEnabled(certificatesapiv1.SchemeGroupVersion.WithResource(resource)) { | 	if resource := "certificatesigningrequests"; apiResourceConfigSource.ResourceEnabled(certificatesapiv1.SchemeGroupVersion.WithResource(resource)) { | ||||||
| 		csrStorage, csrStatusStorage, csrApprovalStorage, err := certificatestore.NewREST(restOptionsGetter) | 		csrStorage, csrStatusStorage, csrApprovalStorage, err := certificatestore.NewREST(restOptionsGetter) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return storage, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
| 		storage[resource] = csrStorage | 		storage[resource] = csrStorage | ||||||
| 		storage[resource+"/status"] = csrStatusStorage | 		storage[resource+"/status"] = csrStatusStorage | ||||||
| @@ -60,6 +70,24 @@ func (p RESTStorageProvider) v1Storage(apiResourceConfigSource serverstorage.API | |||||||
| 	return storage, nil | 	return storage, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (p RESTStorageProvider) v1alpha1Storage(apiResourceConfigSource serverstorage.APIResourceConfigSource, restOptionsGetter generic.RESTOptionsGetter) (map[string]rest.Storage, error) { | ||||||
|  | 	storage := map[string]rest.Storage{} | ||||||
|  |  | ||||||
|  | 	if resource := "clustertrustbundles"; apiResourceConfigSource.ResourceEnabled(certificatesapiv1alpha1.SchemeGroupVersion.WithResource(resource)) { | ||||||
|  | 		if utilfeature.DefaultFeatureGate.Enabled(features.ClusterTrustBundle) { | ||||||
|  | 			bundleStorage, err := clustertrustbundlestore.NewREST(restOptionsGetter) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return nil, err | ||||||
|  | 			} | ||||||
|  | 			storage[resource] = bundleStorage | ||||||
|  | 		} else { | ||||||
|  | 			klog.Warning("ClusterTrustBundle storage is disabled because the ClusterTrustBundle feature gate is disabled") | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return storage, nil | ||||||
|  | } | ||||||
|  |  | ||||||
| func (p RESTStorageProvider) GroupName() string { | func (p RESTStorageProvider) GroupName() string { | ||||||
| 	return certificates.GroupName | 	return certificates.GroupName | ||||||
| } | } | ||||||
|   | |||||||
| @@ -42,11 +42,11 @@ func NewEtcdStorageForResource(t *testing.T, resource schema.GroupResource) (*st | |||||||
| 	completedConfig.APIResourceConfig = serverstorage.NewResourceConfig() | 	completedConfig.APIResourceConfig = serverstorage.NewResourceConfig() | ||||||
| 	factory, err := completedConfig.New() | 	factory, err := completedConfig.New() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatalf("Error while making storage factory: %v", err) | ||||||
| 	} | 	} | ||||||
| 	resourceConfig, err := factory.NewConfig(resource) | 	resourceConfig, err := factory.NewConfig(resource) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatal(err) | 		t.Fatalf("Error while finding storage destination: %v", err) | ||||||
| 	} | 	} | ||||||
| 	return resourceConfig, server | 	return resourceConfig, server | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										133
									
								
								plugin/pkg/admission/certificates/ctbattest/admission.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								plugin/pkg/admission/certificates/ctbattest/admission.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,133 @@ | |||||||
|  | /* | ||||||
|  | Copyright 2022 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 ctbattest | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  |  | ||||||
|  | 	"k8s.io/apiserver/pkg/admission" | ||||||
|  | 	genericadmissioninit "k8s.io/apiserver/pkg/admission/initializer" | ||||||
|  | 	"k8s.io/apiserver/pkg/authorization/authorizer" | ||||||
|  | 	"k8s.io/component-base/featuregate" | ||||||
|  | 	"k8s.io/klog/v2" | ||||||
|  | 	api "k8s.io/kubernetes/pkg/apis/certificates" | ||||||
|  | 	"k8s.io/kubernetes/pkg/features" | ||||||
|  | 	"k8s.io/kubernetes/plugin/pkg/admission/certificates" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | const PluginName = "ClusterTrustBundleAttest" | ||||||
|  |  | ||||||
|  | func Register(plugins *admission.Plugins) { | ||||||
|  | 	plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) { | ||||||
|  | 		return NewPlugin(), nil | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Plugin is the ClusterTrustBundle attest plugin. | ||||||
|  | // | ||||||
|  | // In order to create or update a ClusterTrustBundle that sets signerName, | ||||||
|  | // you must have the following permission: group=certificates.k8s.io | ||||||
|  | // resource=signers resourceName=<the signer name> verb=attest. | ||||||
|  | type Plugin struct { | ||||||
|  | 	*admission.Handler | ||||||
|  | 	authz authorizer.Authorizer | ||||||
|  |  | ||||||
|  | 	inspectedFeatureGates bool | ||||||
|  | 	enabled               bool | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var _ admission.ValidationInterface = &Plugin{} | ||||||
|  | var _ admission.InitializationValidator = &Plugin{} | ||||||
|  | var _ genericadmissioninit.WantsAuthorizer = &Plugin{} | ||||||
|  | var _ genericadmissioninit.WantsFeatures = &Plugin{} | ||||||
|  |  | ||||||
|  | func NewPlugin() *Plugin { | ||||||
|  | 	return &Plugin{ | ||||||
|  | 		Handler: admission.NewHandler(admission.Create, admission.Update), | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // SetAuthorizer sets the plugin's authorizer. | ||||||
|  | func (p *Plugin) SetAuthorizer(authz authorizer.Authorizer) { | ||||||
|  | 	p.authz = authz | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // InspectFeatureGates implements WantsFeatures. | ||||||
|  | func (p *Plugin) InspectFeatureGates(featureGates featuregate.FeatureGate) { | ||||||
|  | 	p.enabled = featureGates.Enabled(features.ClusterTrustBundle) | ||||||
|  | 	p.inspectedFeatureGates = true | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ValidateInitialization checks that the plugin was initialized correctly. | ||||||
|  | func (p *Plugin) ValidateInitialization() error { | ||||||
|  | 	if p.authz == nil { | ||||||
|  | 		return fmt.Errorf("%s requires an authorizer", PluginName) | ||||||
|  | 	} | ||||||
|  | 	if !p.inspectedFeatureGates { | ||||||
|  | 		return fmt.Errorf("%s did not see feature gates", PluginName) | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var clusterTrustBundleGroupResource = api.Resource("clustertrustbundles") | ||||||
|  |  | ||||||
|  | func (p *Plugin) Validate(ctx context.Context, a admission.Attributes, _ admission.ObjectInterfaces) error { | ||||||
|  | 	if !p.enabled { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 	if a.GetResource().GroupResource() != clusterTrustBundleGroupResource { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	newBundle, ok := a.GetObject().(*api.ClusterTrustBundle) | ||||||
|  | 	if !ok { | ||||||
|  | 		return admission.NewForbidden(a, fmt.Errorf("expected type ClusterTrustBundle, got: %T", a.GetOldObject())) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Unlike CSRs, it's OK to validate against the *new* object, because | ||||||
|  | 	// updates to signer name will be rejected during validation.  For defense | ||||||
|  | 	// in depth, reject attempts to change signer at this layer as well. | ||||||
|  | 	// | ||||||
|  | 	// We want to use the new object because we also need to perform the signer | ||||||
|  | 	// name permission check on *create*. | ||||||
|  |  | ||||||
|  | 	if a.GetOperation() == admission.Update { | ||||||
|  | 		oldBundle, ok := a.GetOldObject().(*api.ClusterTrustBundle) | ||||||
|  | 		if !ok { | ||||||
|  | 			return admission.NewForbidden(a, fmt.Errorf("expected type ClusterTrustBundle, got: %T", a.GetOldObject())) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if oldBundle.Spec.SignerName != newBundle.Spec.SignerName { | ||||||
|  | 			return admission.NewForbidden(a, fmt.Errorf("changing signerName is forbidden")) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// If signer name isn't specified, we don't need to perform the | ||||||
|  | 	// attest check. | ||||||
|  | 	if newBundle.Spec.SignerName == "" { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if !certificates.IsAuthorizedForSignerName(ctx, p.authz, a.GetUserInfo(), "attest", newBundle.Spec.SignerName) { | ||||||
|  | 		klog.V(4).Infof("user not permitted to attest ClusterTrustBundle %q with signerName %q", newBundle.Name, newBundle.Spec.SignerName) | ||||||
|  | 		return admission.NewForbidden(a, fmt.Errorf("user not permitted to attest for signerName %q", newBundle.Spec.SignerName)) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										306
									
								
								plugin/pkg/admission/certificates/ctbattest/admission_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										306
									
								
								plugin/pkg/admission/certificates/ctbattest/admission_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,306 @@ | |||||||
|  | /* | ||||||
|  | Copyright 2022 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 ctbattest | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"k8s.io/apimachinery/pkg/runtime" | ||||||
|  | 	"k8s.io/apimachinery/pkg/runtime/schema" | ||||||
|  | 	"k8s.io/apiserver/pkg/admission" | ||||||
|  | 	"k8s.io/apiserver/pkg/authentication/user" | ||||||
|  | 	"k8s.io/apiserver/pkg/authorization/authorizer" | ||||||
|  | 	"k8s.io/apiserver/pkg/util/feature" | ||||||
|  | 	featuregatetesting "k8s.io/component-base/featuregate/testing" | ||||||
|  | 	certificatesapi "k8s.io/kubernetes/pkg/apis/certificates" | ||||||
|  | 	"k8s.io/kubernetes/pkg/features" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestPluginValidate(t *testing.T) { | ||||||
|  | 	tests := []struct { | ||||||
|  | 		description                      string | ||||||
|  | 		clusterTrustBundleFeatureEnabled bool | ||||||
|  | 		attributes                       admission.Attributes | ||||||
|  | 		allowedName                      string | ||||||
|  | 		allowed                          bool | ||||||
|  | 		authzErr                         error | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			description:                      "wrong type on create", | ||||||
|  | 			clusterTrustBundleFeatureEnabled: true, | ||||||
|  | 			attributes: &testAttributes{ | ||||||
|  | 				resource:  certificatesapi.Resource("clustertrustbundles"), | ||||||
|  | 				obj:       &certificatesapi.ClusterTrustBundleList{}, | ||||||
|  | 				operation: admission.Create, | ||||||
|  | 			}, | ||||||
|  | 			allowed: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			description:                      "wrong type on update", | ||||||
|  | 			clusterTrustBundleFeatureEnabled: true, | ||||||
|  | 			attributes: &testAttributes{ | ||||||
|  | 				resource:  certificatesapi.Resource("clustertrustbundles"), | ||||||
|  | 				obj:       &certificatesapi.ClusterTrustBundleList{}, | ||||||
|  | 				operation: admission.Update, | ||||||
|  | 			}, | ||||||
|  | 			allowed: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			description:                      "reject requests if looking up permissions fails", | ||||||
|  | 			clusterTrustBundleFeatureEnabled: true, | ||||||
|  | 			attributes: &testAttributes{ | ||||||
|  | 				resource: certificatesapi.Resource("clustertrustbundles"), | ||||||
|  | 				obj: &certificatesapi.ClusterTrustBundle{ | ||||||
|  | 					Spec: certificatesapi.ClusterTrustBundleSpec{ | ||||||
|  | 						SignerName: "abc.com/xyz", | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 				operation: admission.Update, | ||||||
|  | 			}, | ||||||
|  | 			authzErr: errors.New("forced error"), | ||||||
|  | 			allowed:  false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			description:                      "should allow create if no signer name is specified", | ||||||
|  | 			clusterTrustBundleFeatureEnabled: true, | ||||||
|  | 			allowedName:                      "abc.com/xyz", | ||||||
|  | 			attributes: &testAttributes{ | ||||||
|  | 				resource: certificatesapi.Resource("clustertrustbundles"), | ||||||
|  | 				obj: &certificatesapi.ClusterTrustBundle{ | ||||||
|  | 					Spec: certificatesapi.ClusterTrustBundleSpec{}, | ||||||
|  | 				}, | ||||||
|  | 				operation: admission.Create, | ||||||
|  | 			}, | ||||||
|  | 			allowed: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			description:                      "should allow update if no signer name is specified", | ||||||
|  | 			clusterTrustBundleFeatureEnabled: true, | ||||||
|  | 			allowedName:                      "abc.com/xyz", | ||||||
|  | 			attributes: &testAttributes{ | ||||||
|  | 				resource: certificatesapi.Resource("clustertrustbundles"), | ||||||
|  | 				oldObj: &certificatesapi.ClusterTrustBundle{ | ||||||
|  | 					Spec: certificatesapi.ClusterTrustBundleSpec{}, | ||||||
|  | 				}, | ||||||
|  | 				obj: &certificatesapi.ClusterTrustBundle{ | ||||||
|  | 					Spec: certificatesapi.ClusterTrustBundleSpec{}, | ||||||
|  | 				}, | ||||||
|  | 				operation: admission.Update, | ||||||
|  | 			}, | ||||||
|  | 			allowed: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			description:                      "should allow create if user is authorized for specific signerName", | ||||||
|  | 			clusterTrustBundleFeatureEnabled: true, | ||||||
|  | 			allowedName:                      "abc.com/xyz", | ||||||
|  | 			attributes: &testAttributes{ | ||||||
|  | 				resource: certificatesapi.Resource("clustertrustbundles"), | ||||||
|  | 				obj: &certificatesapi.ClusterTrustBundle{ | ||||||
|  | 					Spec: certificatesapi.ClusterTrustBundleSpec{ | ||||||
|  | 						SignerName: "abc.com/xyz", | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 				operation: admission.Create, | ||||||
|  | 			}, | ||||||
|  | 			allowed: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			description:                      "should allow update if user is authorized for specific signerName", | ||||||
|  | 			clusterTrustBundleFeatureEnabled: true, | ||||||
|  | 			allowedName:                      "abc.com/xyz", | ||||||
|  | 			attributes: &testAttributes{ | ||||||
|  | 				resource: certificatesapi.Resource("clustertrustbundles"), | ||||||
|  | 				oldObj: &certificatesapi.ClusterTrustBundle{ | ||||||
|  | 					Spec: certificatesapi.ClusterTrustBundleSpec{ | ||||||
|  | 						SignerName: "abc.com/xyz", | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 				obj: &certificatesapi.ClusterTrustBundle{ | ||||||
|  | 					Spec: certificatesapi.ClusterTrustBundleSpec{ | ||||||
|  | 						SignerName: "abc.com/xyz", | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 				operation: admission.Update, | ||||||
|  | 			}, | ||||||
|  | 			allowed: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			description:                      "should allow create if user is authorized with wildcard", | ||||||
|  | 			clusterTrustBundleFeatureEnabled: true, | ||||||
|  | 			allowedName:                      "abc.com/*", | ||||||
|  | 			attributes: &testAttributes{ | ||||||
|  | 				resource: certificatesapi.Resource("clustertrustbundles"), | ||||||
|  | 				obj: &certificatesapi.ClusterTrustBundle{ | ||||||
|  | 					Spec: certificatesapi.ClusterTrustBundleSpec{ | ||||||
|  | 						SignerName: "abc.com/xyz", | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 				operation: admission.Create, | ||||||
|  | 			}, | ||||||
|  | 			allowed: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			description:                      "should allow update if user is authorized with wildcard", | ||||||
|  | 			clusterTrustBundleFeatureEnabled: true, | ||||||
|  | 			allowedName:                      "abc.com/*", | ||||||
|  | 			attributes: &testAttributes{ | ||||||
|  | 				resource: certificatesapi.Resource("clustertrustbundles"), | ||||||
|  | 				oldObj: &certificatesapi.ClusterTrustBundle{ | ||||||
|  | 					Spec: certificatesapi.ClusterTrustBundleSpec{ | ||||||
|  | 						SignerName: "abc.com/xyz", | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 				obj: &certificatesapi.ClusterTrustBundle{ | ||||||
|  | 					Spec: certificatesapi.ClusterTrustBundleSpec{ | ||||||
|  | 						SignerName: "abc.com/xyz", | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 				operation: admission.Update, | ||||||
|  | 			}, | ||||||
|  | 			allowed: true, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			description:                      "should deny create if user does not have permission for this signerName", | ||||||
|  | 			clusterTrustBundleFeatureEnabled: true, | ||||||
|  | 			allowedName:                      "notabc.com/xyz", | ||||||
|  | 			attributes: &testAttributes{ | ||||||
|  | 				resource: certificatesapi.Resource("clustertrustbundles"), | ||||||
|  | 				obj: &certificatesapi.ClusterTrustBundle{ | ||||||
|  | 					Spec: certificatesapi.ClusterTrustBundleSpec{ | ||||||
|  | 						SignerName: "abc.com/xyz", | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 				operation: admission.Create, | ||||||
|  | 			}, | ||||||
|  | 			allowed: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			description:                      "should deny update if user does not have permission for this signerName", | ||||||
|  | 			clusterTrustBundleFeatureEnabled: true, | ||||||
|  | 			allowedName:                      "notabc.com/xyz", | ||||||
|  | 			attributes: &testAttributes{ | ||||||
|  | 				resource: certificatesapi.Resource("clustertrustbundles"), | ||||||
|  | 				obj: &certificatesapi.ClusterTrustBundle{ | ||||||
|  | 					Spec: certificatesapi.ClusterTrustBundleSpec{ | ||||||
|  | 						SignerName: "abc.com/xyz", | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 				operation: admission.Update, | ||||||
|  | 			}, | ||||||
|  | 			allowed: false, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, tc := range tests { | ||||||
|  | 		t.Run(tc.description, func(t *testing.T) { | ||||||
|  | 			p := Plugin{ | ||||||
|  | 				authz: fakeAuthorizer{ | ||||||
|  | 					t:           t, | ||||||
|  | 					verb:        "attest", | ||||||
|  | 					allowedName: tc.allowedName, | ||||||
|  | 					decision:    authorizer.DecisionAllow, | ||||||
|  | 					err:         tc.authzErr, | ||||||
|  | 				}, | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			defer featuregatetesting.SetFeatureGateDuringTest(t, feature.DefaultFeatureGate, features.ClusterTrustBundle, tc.clusterTrustBundleFeatureEnabled)() | ||||||
|  | 			p.InspectFeatureGates(feature.DefaultFeatureGate) | ||||||
|  |  | ||||||
|  | 			err := p.Validate(context.Background(), tc.attributes, nil) | ||||||
|  | 			if err == nil && !tc.allowed { | ||||||
|  | 				t.Errorf("Expected authorization policy to reject ClusterTrustBundle but it was allowed") | ||||||
|  | 			} | ||||||
|  | 			if err != nil && tc.allowed { | ||||||
|  | 				t.Errorf("Expected authorization policy to accept ClusterTrustBundle but it was rejected: %v", err) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type fakeAuthorizer struct { | ||||||
|  | 	t           *testing.T | ||||||
|  | 	verb        string | ||||||
|  | 	allowedName string | ||||||
|  | 	decision    authorizer.Decision | ||||||
|  | 	err         error | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (f fakeAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) { | ||||||
|  | 	if f.err != nil { | ||||||
|  | 		return f.decision, "forced error", f.err | ||||||
|  | 	} | ||||||
|  | 	if a.GetVerb() != f.verb { | ||||||
|  | 		return authorizer.DecisionDeny, fmt.Sprintf("unrecognised verb '%s'", a.GetVerb()), nil | ||||||
|  | 	} | ||||||
|  | 	if a.GetAPIGroup() != "certificates.k8s.io" { | ||||||
|  | 		return authorizer.DecisionDeny, fmt.Sprintf("unrecognised groupName '%s'", a.GetAPIGroup()), nil | ||||||
|  | 	} | ||||||
|  | 	if a.GetAPIVersion() != "*" { | ||||||
|  | 		return authorizer.DecisionDeny, fmt.Sprintf("unrecognised apiVersion '%s'", a.GetAPIVersion()), nil | ||||||
|  | 	} | ||||||
|  | 	if a.GetResource() != "signers" { | ||||||
|  | 		return authorizer.DecisionDeny, fmt.Sprintf("unrecognised resource '%s'", a.GetResource()), nil | ||||||
|  | 	} | ||||||
|  | 	if a.GetName() != f.allowedName { | ||||||
|  | 		return authorizer.DecisionDeny, fmt.Sprintf("unrecognised resource name '%s'", a.GetName()), nil | ||||||
|  | 	} | ||||||
|  | 	if !a.IsResourceRequest() { | ||||||
|  | 		return authorizer.DecisionDeny, fmt.Sprintf("unrecognised IsResourceRequest '%t'", a.IsResourceRequest()), nil | ||||||
|  | 	} | ||||||
|  | 	return f.decision, "", nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type testAttributes struct { | ||||||
|  | 	resource    schema.GroupResource | ||||||
|  | 	subresource string | ||||||
|  | 	operation   admission.Operation | ||||||
|  | 	obj, oldObj runtime.Object | ||||||
|  | 	name        string | ||||||
|  |  | ||||||
|  | 	admission.Attributes // nil panic if any other methods called | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t *testAttributes) GetResource() schema.GroupVersionResource { | ||||||
|  | 	return t.resource.WithVersion("ignored") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t *testAttributes) GetSubresource() string { | ||||||
|  | 	return t.subresource | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t *testAttributes) GetObject() runtime.Object { | ||||||
|  | 	return t.obj | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t *testAttributes) GetOldObject() runtime.Object { | ||||||
|  | 	return t.oldObj | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t *testAttributes) GetName() string { | ||||||
|  | 	return t.name | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t *testAttributes) GetOperation() admission.Operation { | ||||||
|  | 	return t.operation | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (t *testAttributes) GetUserInfo() user.Info { | ||||||
|  | 	return &user.DefaultInfo{Name: "ignored"} | ||||||
|  | } | ||||||
| @@ -180,6 +180,10 @@ func NodeRules() []rbacv1.PolicyRule { | |||||||
| 	if utilfeature.DefaultFeatureGate.Enabled(features.DynamicResourceAllocation) { | 	if utilfeature.DefaultFeatureGate.Enabled(features.DynamicResourceAllocation) { | ||||||
| 		nodePolicyRules = append(nodePolicyRules, rbacv1helpers.NewRule("get").Groups(resourceGroup).Resources("resourceclaims").RuleOrDie()) | 		nodePolicyRules = append(nodePolicyRules, rbacv1helpers.NewRule("get").Groups(resourceGroup).Resources("resourceclaims").RuleOrDie()) | ||||||
| 	} | 	} | ||||||
|  | 	// Kubelet needs access to ClusterTrustBundles to support the pemTrustAnchors volume type. | ||||||
|  | 	if utilfeature.DefaultFeatureGate.Enabled(features.ClusterTrustBundle) { | ||||||
|  | 		nodePolicyRules = append(nodePolicyRules, rbacv1helpers.NewRule("get", "list", "watch").Groups(certificatesGroup).Resources("clustertrustbundles").RuleOrDie()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	return nodePolicyRules | 	return nodePolicyRules | ||||||
| } | } | ||||||
| @@ -585,6 +589,16 @@ func ClusterRoles() []rbacv1.ClusterRole { | |||||||
| 		Rules:      kubeSchedulerRules, | 		Rules:      kubeSchedulerRules, | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
|  | 	// Default ClusterRole to allow reading ClusterTrustBundle objects | ||||||
|  | 	if utilfeature.DefaultFeatureGate.Enabled(features.ClusterTrustBundle) { | ||||||
|  | 		roles = append(roles, rbacv1.ClusterRole{ | ||||||
|  | 			ObjectMeta: metav1.ObjectMeta{Name: "system:cluster-trust-bundle-discovery"}, | ||||||
|  | 			Rules: []rbacv1.PolicyRule{ | ||||||
|  | 				rbacv1helpers.NewRule(Read...).Groups(certificatesGroup).Resources("clustertrustbundles").RuleOrDie(), | ||||||
|  | 			}, | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	addClusterRoleLabel(roles) | 	addClusterRoleLabel(roles) | ||||||
| 	return roles | 	return roles | ||||||
| } | } | ||||||
| @@ -625,6 +639,11 @@ func ClusterRoleBindings() []rbacv1.ClusterRoleBinding { | |||||||
| 		rbacv1helpers.NewClusterBinding("system:service-account-issuer-discovery").Groups(serviceaccount.AllServiceAccountsGroup).BindingOrDie(), | 		rbacv1helpers.NewClusterBinding("system:service-account-issuer-discovery").Groups(serviceaccount.AllServiceAccountsGroup).BindingOrDie(), | ||||||
| 	) | 	) | ||||||
|  |  | ||||||
|  | 	// Service accounts can read ClusterTrustBundle objects. | ||||||
|  | 	if utilfeature.DefaultFeatureGate.Enabled(features.ClusterTrustBundle) { | ||||||
|  | 		rolebindings = append(rolebindings, rbacv1helpers.NewClusterBinding("system:cluster-trust-bundle-discovery").Groups(serviceaccount.AllServiceAccountsGroup).BindingOrDie()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	addClusterRoleBindingLabel(rolebindings) | 	addClusterRoleBindingLabel(rolebindings) | ||||||
|  |  | ||||||
| 	return rolebindings | 	return rolebindings | ||||||
|   | |||||||
							
								
								
									
										24
									
								
								staging/src/k8s.io/api/certificates/v1alpha1/doc.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								staging/src/k8s.io/api/certificates/v1alpha1/doc.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | |||||||
|  | /* | ||||||
|  | Copyright 2022 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. | ||||||
|  | */ | ||||||
|  |  | ||||||
|  | // +k8s:deepcopy-gen=package | ||||||
|  | // +k8s:protobuf-gen=package | ||||||
|  | // +k8s:openapi-gen=true | ||||||
|  | // +k8s:prerelease-lifecycle-gen=true | ||||||
|  |  | ||||||
|  | // +groupName=certificates.k8s.io | ||||||
|  |  | ||||||
|  | package v1alpha1 // import "k8s.io/api/certificates/v1alpha1" | ||||||
							
								
								
									
										61
									
								
								staging/src/k8s.io/api/certificates/v1alpha1/register.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								staging/src/k8s.io/api/certificates/v1alpha1/register.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | |||||||
|  | /* | ||||||
|  | Copyright 2022 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 v1alpha1 | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
|  | 	"k8s.io/apimachinery/pkg/runtime" | ||||||
|  | 	"k8s.io/apimachinery/pkg/runtime/schema" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // GroupName is the group name use in this package | ||||||
|  | const GroupName = "certificates.k8s.io" | ||||||
|  |  | ||||||
|  | // SchemeGroupVersion is group version used to register these objects | ||||||
|  | var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"} | ||||||
|  |  | ||||||
|  | // Kind takes an unqualified kind and returns a Group qualified GroupKind | ||||||
|  | func Kind(kind string) schema.GroupKind { | ||||||
|  | 	return SchemeGroupVersion.WithKind(kind).GroupKind() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Resource takes an unqualified resource and returns a Group qualified GroupResource | ||||||
|  | func Resource(resource string) schema.GroupResource { | ||||||
|  | 	return SchemeGroupVersion.WithResource(resource).GroupResource() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	// SchemeBuilder is the scheme builder with scheme init functions to run for this API package | ||||||
|  | 	SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) | ||||||
|  |  | ||||||
|  | 	localSchemeBuilder = &SchemeBuilder | ||||||
|  |  | ||||||
|  | 	// AddToScheme is a global function that registers this API group & version to a scheme | ||||||
|  | 	AddToScheme = localSchemeBuilder.AddToScheme | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // Adds the list of known types to the given scheme. | ||||||
|  | func addKnownTypes(scheme *runtime.Scheme) error { | ||||||
|  | 	scheme.AddKnownTypes(SchemeGroupVersion, | ||||||
|  | 		&ClusterTrustBundle{}, | ||||||
|  | 		&ClusterTrustBundleList{}, | ||||||
|  | 	) | ||||||
|  |  | ||||||
|  | 	// Add the watch version that applies | ||||||
|  | 	metav1.AddToGroupVersion(scheme, SchemeGroupVersion) | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
							
								
								
									
										106
									
								
								staging/src/k8s.io/api/certificates/v1alpha1/types.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								staging/src/k8s.io/api/certificates/v1alpha1/types.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,106 @@ | |||||||
|  | /* | ||||||
|  | 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 v1alpha1 | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // +genclient | ||||||
|  | // +genclient:nonNamespaced | ||||||
|  | // +k8s:prerelease-lifecycle-gen:introduced=1.26 | ||||||
|  | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object | ||||||
|  |  | ||||||
|  | // ClusterTrustBundle is a cluster-scoped container for X.509 trust anchors | ||||||
|  | // (root certificates). | ||||||
|  | // | ||||||
|  | // ClusterTrustBundle objects are considered to be readable by any authenticated | ||||||
|  | // user in the cluster, because they can be mounted by pods using the | ||||||
|  | // `clusterTrustBundle` projection.  All service accounts have read access to | ||||||
|  | // ClusterTrustBundles by default.  Users who only have namespace-level access | ||||||
|  | // to a cluster can read ClusterTrustBundles by impersonating a serviceaccount | ||||||
|  | // that they have access to. | ||||||
|  | // | ||||||
|  | // It can be optionally associated with a particular assigner, in which case it | ||||||
|  | // contains one valid set of trust anchors for that signer. Signers may have | ||||||
|  | // multiple associated ClusterTrustBundles; each is an independent set of trust | ||||||
|  | // anchors for that signer. Admission control is used to enforce that only users | ||||||
|  | // with permissions on the signer can create or modify the corresponding bundle. | ||||||
|  | type ClusterTrustBundle struct { | ||||||
|  | 	metav1.TypeMeta `json:",inline"` | ||||||
|  |  | ||||||
|  | 	// metadata contains the object metadata. | ||||||
|  | 	// +optional | ||||||
|  | 	metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` | ||||||
|  |  | ||||||
|  | 	// spec contains the signer (if any) and trust anchors. | ||||||
|  | 	Spec ClusterTrustBundleSpec `json:"spec" protobuf:"bytes,2,opt,name=spec"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // ClusterTrustBundleSpec contains the signer and trust anchors. | ||||||
|  | type ClusterTrustBundleSpec struct { | ||||||
|  | 	// signerName indicates the associated signer, if any. | ||||||
|  | 	// | ||||||
|  | 	// In order to create or update a ClusterTrustBundle that sets signerName, | ||||||
|  | 	// you must have the following cluster-scoped permission: | ||||||
|  | 	// group=certificates.k8s.io resource=signers resourceName=<the signer name> | ||||||
|  | 	// verb=attest. | ||||||
|  | 	// | ||||||
|  | 	// If signerName is not empty, then the ClusterTrustBundle object must be | ||||||
|  | 	// named with the signer name as a prefix (translating slashes to colons). | ||||||
|  | 	// For example, for the signer name `example.com/foo`, valid | ||||||
|  | 	// ClusterTrustBundle object names include `example.com:foo:abc` and | ||||||
|  | 	// `example.com:foo:v1`. | ||||||
|  | 	// | ||||||
|  | 	// If signerName is empty, then the ClusterTrustBundle object's name must | ||||||
|  | 	// not have such a prefix. | ||||||
|  | 	// | ||||||
|  | 	// List/watch requests for ClusterTrustBundles can filter on this field | ||||||
|  | 	// using a `spec.signerName=NAME` field selector. | ||||||
|  | 	// | ||||||
|  | 	// +optional | ||||||
|  | 	SignerName string `json:"signerName,omitempty" protobuf:"bytes,1,opt,name=signerName"` | ||||||
|  |  | ||||||
|  | 	// trustBundle contains the individual X.509 trust anchors for this | ||||||
|  | 	// bundle, as PEM bundle of PEM-wrapped, DER-formatted X.509 certificates. | ||||||
|  | 	// | ||||||
|  | 	// The data must consist only of PEM certificate blocks that parse as valid | ||||||
|  | 	// X.509 certificates.  Each certificate must include a basic constraints | ||||||
|  | 	// extension with the CA bit set.  The API server will reject objects that | ||||||
|  | 	// contain duplicate certificates, or that use PEM block headers. | ||||||
|  | 	// | ||||||
|  | 	// Users of ClusterTrustBundles, including Kubelet, are free to reorder and | ||||||
|  | 	// deduplicate certificate blocks in this file according to their own logic, | ||||||
|  | 	// as well as to drop PEM block headers and inter-block data. | ||||||
|  | 	TrustBundle string `json:"trustBundle" protobuf:"bytes,2,opt,name=trustBundle"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // +k8s:prerelease-lifecycle-gen:introduced=1.26 | ||||||
|  | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object | ||||||
|  |  | ||||||
|  | // ClusterTrustBundleList is a collection of ClusterTrustBundle objects | ||||||
|  | type ClusterTrustBundleList struct { | ||||||
|  | 	metav1.TypeMeta `json:",inline"` | ||||||
|  | 	 | ||||||
|  | 	// metadata contains the list metadata. | ||||||
|  | 	// | ||||||
|  | 	// +optional | ||||||
|  | 	metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` | ||||||
|  |  | ||||||
|  | 	// items is a collection of ClusterTrustBundle objects | ||||||
|  | 	Items []ClusterTrustBundle `json:"items" protobuf:"bytes,2,rep,name=items"` | ||||||
|  | } | ||||||
| @@ -41,6 +41,7 @@ import ( | |||||||
| 	batchv1 "k8s.io/api/batch/v1" | 	batchv1 "k8s.io/api/batch/v1" | ||||||
| 	batchv1beta1 "k8s.io/api/batch/v1beta1" | 	batchv1beta1 "k8s.io/api/batch/v1beta1" | ||||||
| 	certificatesv1 "k8s.io/api/certificates/v1" | 	certificatesv1 "k8s.io/api/certificates/v1" | ||||||
|  | 	certificatesv1alpha1 "k8s.io/api/certificates/v1alpha1" | ||||||
| 	certificatesv1beta1 "k8s.io/api/certificates/v1beta1" | 	certificatesv1beta1 "k8s.io/api/certificates/v1beta1" | ||||||
| 	coordinationv1 "k8s.io/api/coordination/v1" | 	coordinationv1 "k8s.io/api/coordination/v1" | ||||||
| 	coordinationv1beta1 "k8s.io/api/coordination/v1beta1" | 	coordinationv1beta1 "k8s.io/api/coordination/v1beta1" | ||||||
| @@ -105,6 +106,7 @@ var groups = []runtime.SchemeBuilder{ | |||||||
| 	batchv1.SchemeBuilder, | 	batchv1.SchemeBuilder, | ||||||
| 	certificatesv1.SchemeBuilder, | 	certificatesv1.SchemeBuilder, | ||||||
| 	certificatesv1beta1.SchemeBuilder, | 	certificatesv1beta1.SchemeBuilder, | ||||||
|  | 	certificatesv1alpha1.SchemeBuilder, | ||||||
| 	coordinationv1.SchemeBuilder, | 	coordinationv1.SchemeBuilder, | ||||||
| 	coordinationv1beta1.SchemeBuilder, | 	coordinationv1beta1.SchemeBuilder, | ||||||
| 	corev1.SchemeBuilder, | 	corev1.SchemeBuilder, | ||||||
|   | |||||||
							
								
								
									
										6
									
								
								vendor/modules.txt
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								vendor/modules.txt
									
									
									
									
										vendored
									
									
								
							| @@ -1234,6 +1234,7 @@ k8s.io/api/autoscaling/v2beta2 | |||||||
| k8s.io/api/batch/v1 | k8s.io/api/batch/v1 | ||||||
| k8s.io/api/batch/v1beta1 | k8s.io/api/batch/v1beta1 | ||||||
| k8s.io/api/certificates/v1 | k8s.io/api/certificates/v1 | ||||||
|  | k8s.io/api/certificates/v1alpha1 | ||||||
| k8s.io/api/certificates/v1beta1 | k8s.io/api/certificates/v1beta1 | ||||||
| k8s.io/api/coordination/v1 | k8s.io/api/coordination/v1 | ||||||
| k8s.io/api/coordination/v1beta1 | k8s.io/api/coordination/v1beta1 | ||||||
| @@ -1576,6 +1577,7 @@ k8s.io/client-go/applyconfigurations/autoscaling/v2beta2 | |||||||
| k8s.io/client-go/applyconfigurations/batch/v1 | k8s.io/client-go/applyconfigurations/batch/v1 | ||||||
| k8s.io/client-go/applyconfigurations/batch/v1beta1 | k8s.io/client-go/applyconfigurations/batch/v1beta1 | ||||||
| k8s.io/client-go/applyconfigurations/certificates/v1 | k8s.io/client-go/applyconfigurations/certificates/v1 | ||||||
|  | k8s.io/client-go/applyconfigurations/certificates/v1alpha1 | ||||||
| k8s.io/client-go/applyconfigurations/certificates/v1beta1 | k8s.io/client-go/applyconfigurations/certificates/v1beta1 | ||||||
| k8s.io/client-go/applyconfigurations/coordination/v1 | k8s.io/client-go/applyconfigurations/coordination/v1 | ||||||
| k8s.io/client-go/applyconfigurations/coordination/v1beta1 | k8s.io/client-go/applyconfigurations/coordination/v1beta1 | ||||||
| @@ -1640,6 +1642,7 @@ k8s.io/client-go/informers/batch/v1 | |||||||
| k8s.io/client-go/informers/batch/v1beta1 | k8s.io/client-go/informers/batch/v1beta1 | ||||||
| k8s.io/client-go/informers/certificates | k8s.io/client-go/informers/certificates | ||||||
| k8s.io/client-go/informers/certificates/v1 | k8s.io/client-go/informers/certificates/v1 | ||||||
|  | k8s.io/client-go/informers/certificates/v1alpha1 | ||||||
| k8s.io/client-go/informers/certificates/v1beta1 | k8s.io/client-go/informers/certificates/v1beta1 | ||||||
| k8s.io/client-go/informers/coordination | k8s.io/client-go/informers/coordination | ||||||
| k8s.io/client-go/informers/coordination/v1 | k8s.io/client-go/informers/coordination/v1 | ||||||
| @@ -1726,6 +1729,8 @@ k8s.io/client-go/kubernetes/typed/batch/v1beta1 | |||||||
| k8s.io/client-go/kubernetes/typed/batch/v1beta1/fake | k8s.io/client-go/kubernetes/typed/batch/v1beta1/fake | ||||||
| k8s.io/client-go/kubernetes/typed/certificates/v1 | k8s.io/client-go/kubernetes/typed/certificates/v1 | ||||||
| k8s.io/client-go/kubernetes/typed/certificates/v1/fake | k8s.io/client-go/kubernetes/typed/certificates/v1/fake | ||||||
|  | k8s.io/client-go/kubernetes/typed/certificates/v1alpha1 | ||||||
|  | k8s.io/client-go/kubernetes/typed/certificates/v1alpha1/fake | ||||||
| k8s.io/client-go/kubernetes/typed/certificates/v1beta1 | k8s.io/client-go/kubernetes/typed/certificates/v1beta1 | ||||||
| k8s.io/client-go/kubernetes/typed/certificates/v1beta1/fake | k8s.io/client-go/kubernetes/typed/certificates/v1beta1/fake | ||||||
| k8s.io/client-go/kubernetes/typed/coordination/v1 | k8s.io/client-go/kubernetes/typed/coordination/v1 | ||||||
| @@ -1802,6 +1807,7 @@ k8s.io/client-go/listers/autoscaling/v2beta2 | |||||||
| k8s.io/client-go/listers/batch/v1 | k8s.io/client-go/listers/batch/v1 | ||||||
| k8s.io/client-go/listers/batch/v1beta1 | k8s.io/client-go/listers/batch/v1beta1 | ||||||
| k8s.io/client-go/listers/certificates/v1 | k8s.io/client-go/listers/certificates/v1 | ||||||
|  | k8s.io/client-go/listers/certificates/v1alpha1 | ||||||
| k8s.io/client-go/listers/certificates/v1beta1 | k8s.io/client-go/listers/certificates/v1beta1 | ||||||
| k8s.io/client-go/listers/coordination/v1 | k8s.io/client-go/listers/coordination/v1 | ||||||
| k8s.io/client-go/listers/coordination/v1beta1 | k8s.io/client-go/listers/coordination/v1beta1 | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Taahir Ahmed
					Taahir Ahmed