mirror of
				https://github.com/optim-enterprises-bv/kubernetes.git
				synced 2025-10-31 18:28: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: "v2alpha1"}:                        {group: 17400, version: 9}, | ||||
| 	{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: "v1alpha1"}:            {group: 17200, version: 1}, | ||||
| 	{Group: "policy", Version: "v1"}:                             {group: 17100, version: 15}, | ||||
|   | ||||
| @@ -88,6 +88,7 @@ batch/v1 \ | ||||
| batch/v1beta1 \ | ||||
| certificates.k8s.io/v1 \ | ||||
| certificates.k8s.io/v1beta1 \ | ||||
| certificates.k8s.io/v1alpha1 \ | ||||
| coordination.k8s.io/v1beta1 \ | ||||
| coordination.k8s.io/v1 \ | ||||
| discovery.k8s.io/v1 \ | ||||
|   | ||||
| @@ -24,6 +24,7 @@ import ( | ||||
| 	"k8s.io/kubernetes/pkg/api/legacyscheme" | ||||
| 	"k8s.io/kubernetes/pkg/apis/certificates" | ||||
| 	v1 "k8s.io/kubernetes/pkg/apis/certificates/v1" | ||||
| 	"k8s.io/kubernetes/pkg/apis/certificates/v1alpha1" | ||||
| 	"k8s.io/kubernetes/pkg/apis/certificates/v1beta1" | ||||
| ) | ||||
|  | ||||
| @@ -36,5 +37,6 @@ func Install(scheme *runtime.Scheme) { | ||||
| 	utilruntime.Must(certificates.AddToScheme(scheme)) | ||||
| 	utilruntime.Must(v1.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, | ||||
| 		&CertificateSigningRequest{}, | ||||
| 		&CertificateSigningRequestList{}, | ||||
| 		&ClusterTrustBundle{}, | ||||
| 		&ClusterTrustBundleList{}, | ||||
| 	) | ||||
| 	return nil | ||||
| } | ||||
|   | ||||
| @@ -224,3 +224,56 @@ const ( | ||||
| 	UsageMicrosoftSGC      KeyUsage = "microsoft 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" | ||||
| 	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/sets" | ||||
| 	utilvalidation "k8s.io/apimachinery/pkg/util/validation" | ||||
| @@ -197,7 +198,7 @@ func validateCertificateSigningRequest(csr *certificates.CertificateSigningReque | ||||
| 	if !opts.allowLegacySignerName && csr.Spec.SignerName == certificates.LegacyUnknownSignerName { | ||||
| 		allErrs = append(allErrs, field.Invalid(specPath.Child("signerName"), csr.Spec.SignerName, "the legacy signerName is not allowed via this API version")) | ||||
| 	} else { | ||||
| 		allErrs = append(allErrs, 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 { | ||||
| 		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 resource name is 253 characters (DNS1123Subdomain max length) | ||||
| // 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 | ||||
| 	if len(signerName) == 0 { | ||||
| 		el = append(el, field.Required(fldPath, "")) | ||||
| @@ -537,3 +538,129 @@ func hasDuplicateUsage(usages []certificates.KeyUsage) bool { | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| // We require your name to be prefixed by .spec.signerName | ||||
| func validateClusterTrustBundleName(signerName string) func(name string, prefix bool) []string { | ||||
| 	return func(name string, isPrefix bool) []string { | ||||
| 		if signerName == "" { | ||||
| 			if strings.Contains(name, ":") { | ||||
| 				return []string{"ClusterTrustBundle without signer name must not have \":\" in its name"} | ||||
| 			} | ||||
| 			return apimachineryvalidation.NameIsDNSSubdomain(name, isPrefix) | ||||
| 		} | ||||
|  | ||||
| 		requiredPrefix := strings.ReplaceAll(signerName, "/", ":") + ":" | ||||
| 		if !strings.HasPrefix(name, requiredPrefix) { | ||||
| 			return []string{fmt.Sprintf("ClusterTrustBundle for signerName %s must be named with prefix %s", signerName, requiredPrefix)} | ||||
| 		} | ||||
| 		return apimachineryvalidation.NameIsDNSSubdomain(strings.TrimPrefix(name, requiredPrefix), isPrefix) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| type ValidateClusterTrustBundleOptions struct { | ||||
| 	SuppressBundleParsing bool | ||||
| } | ||||
|  | ||||
| // 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" | ||||
| 	"encoding/pem" | ||||
| 	"fmt" | ||||
| 	"math/big" | ||||
| 	mathrand "math/rand" | ||||
| 	"reflect" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/google/go-cmp/cmp" | ||||
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||||
| 	"k8s.io/apimachinery/pkg/util/sets" | ||||
| 	"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 ( | ||||
| 	validCertificate = []byte(` | ||||
| Leading non-PEM content | ||||
|   | ||||
| @@ -40,6 +40,7 @@ import ( | ||||
| 	batchapiv1 "k8s.io/api/batch/v1" | ||||
| 	batchapiv1beta1 "k8s.io/api/batch/v1beta1" | ||||
| 	certificatesapiv1 "k8s.io/api/certificates/v1" | ||||
| 	certificatesv1alpha1 "k8s.io/api/certificates/v1alpha1" | ||||
| 	coordinationapiv1 "k8s.io/api/coordination/v1" | ||||
| 	apiv1 "k8s.io/api/core/v1" | ||||
| 	discoveryv1 "k8s.io/api/discovery/v1" | ||||
| @@ -734,6 +735,7 @@ var ( | ||||
| 		apiserverinternalv1alpha1.SchemeGroupVersion, | ||||
| 		authenticationv1alpha1.SchemeGroupVersion, | ||||
| 		resourcev1alpha2.SchemeGroupVersion, | ||||
| 		certificatesv1alpha1.SchemeGroupVersion, | ||||
| 		networkingapiv1alpha1.SchemeGroupVersion, | ||||
| 		storageapiv1alpha1.SchemeGroupVersion, | ||||
| 		flowcontrolv1alpha1.SchemeGroupVersion, | ||||
|   | ||||
| @@ -67,6 +67,12 @@ const ( | ||||
| 	// Enables dual-stack --node-ip in kubelet with external cloud providers | ||||
| 	CloudDualStackNodeIPs featuregate.Feature = "CloudDualStackNodeIPs" | ||||
|  | ||||
| 	// owner: @ahmedtd | ||||
| 	// alpha: v1.26 | ||||
| 	// | ||||
| 	// Enable ClusterTrustBundle objects and Kubelet integration. | ||||
| 	ClusterTrustBundle featuregate.Feature = "ClusterTrustBundle" | ||||
|  | ||||
| 	// owner: @szuecs | ||||
| 	// alpha: v1.12 | ||||
| 	// | ||||
| @@ -934,6 +940,8 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS | ||||
|  | ||||
| 	CloudDualStackNodeIPs: {Default: false, PreRelease: featuregate.Alpha}, | ||||
|  | ||||
| 	ClusterTrustBundle: {Default: false, PreRelease: featuregate.Alpha}, | ||||
|  | ||||
| 	CPUCFSQuotaPeriod: {Default: false, PreRelease: featuregate.Alpha}, | ||||
|  | ||||
| 	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/apis/admissionregistration" | ||||
| 	"k8s.io/kubernetes/pkg/apis/apps" | ||||
| 	"k8s.io/kubernetes/pkg/apis/certificates" | ||||
| 	api "k8s.io/kubernetes/pkg/apis/core" | ||||
| 	"k8s.io/kubernetes/pkg/apis/events" | ||||
| 	"k8s.io/kubernetes/pkg/apis/extensions" | ||||
| @@ -72,6 +73,7 @@ func NewStorageFactoryConfig() *StorageFactoryConfig { | ||||
| 		admissionregistration.Resource("validatingadmissionpolicybindings").WithVersion("v1alpha1"), | ||||
| 		networking.Resource("clustercidrs").WithVersion("v1alpha1"), | ||||
| 		networking.Resource("ipaddresses").WithVersion("v1alpha1"), | ||||
| 		certificates.Resource("clustertrustbundles").WithVersion("v1alpha1"), | ||||
| 	} | ||||
|  | ||||
| 	return &StorageFactoryConfig{ | ||||
|   | ||||
| @@ -26,6 +26,7 @@ import ( | ||||
| 	"k8s.io/kubernetes/plugin/pkg/admission/alwayspullimages" | ||||
| 	"k8s.io/kubernetes/plugin/pkg/admission/antiaffinity" | ||||
| 	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" | ||||
| 	certsubjectrestriction "k8s.io/kubernetes/plugin/pkg/admission/certificates/subjectrestriction" | ||||
| 	"k8s.io/kubernetes/plugin/pkg/admission/defaulttolerationseconds" | ||||
| @@ -90,6 +91,7 @@ var AllOrderedPlugins = []string{ | ||||
| 	runtimeclass.PluginName,                 // RuntimeClass | ||||
| 	certapproval.PluginName,                 // CertificateApproval | ||||
| 	certsigning.PluginName,                  // CertificateSigning | ||||
| 	ctbattest.PluginName,                    // ClusterTrustBundleAttest | ||||
| 	certsubjectrestriction.PluginName,       // CertificateSubjectRestriction | ||||
| 	defaultingressclass.PluginName,          // DefaultIngressClass | ||||
| 	denyserviceexternalips.PluginName,       // DenyServiceExternalIPs | ||||
| @@ -137,6 +139,7 @@ func RegisterAllAdmissionPlugins(plugins *admission.Plugins) { | ||||
| 	storageobjectinuseprotection.Register(plugins) | ||||
| 	certapproval.Register(plugins) | ||||
| 	certsigning.Register(plugins) | ||||
| 	ctbattest.Register(plugins) | ||||
| 	certsubjectrestriction.Register(plugins) | ||||
| } | ||||
|  | ||||
| @@ -158,6 +161,7 @@ func DefaultOffAdmissionPlugins() sets.String { | ||||
| 		runtimeclass.PluginName,                 // RuntimeClass | ||||
| 		certapproval.PluginName,                 // CertificateApproval | ||||
| 		certsigning.PluginName,                  // CertificateSigning | ||||
| 		ctbattest.PluginName,                    // ClusterTrustBundleAttest | ||||
| 		certsubjectrestriction.PluginName,       // CertificateSubjectRestriction | ||||
| 		defaultingressclass.PluginName,          // DefaultIngressClass | ||||
| 		podsecurity.PluginName,                  // PodSecurity | ||||
|   | ||||
| @@ -31,6 +31,7 @@ import ( | ||||
| 	autoscalingv2beta1 "k8s.io/api/autoscaling/v2beta1" | ||||
| 	batchv1 "k8s.io/api/batch/v1" | ||||
| 	batchv1beta1 "k8s.io/api/batch/v1beta1" | ||||
| 	certificatesv1alpha1 "k8s.io/api/certificates/v1alpha1" | ||||
| 	certificatesv1beta1 "k8s.io/api/certificates/v1beta1" | ||||
| 	coordinationv1 "k8s.io/api/coordination/v1" | ||||
| 	apiv1 "k8s.io/api/core/v1" | ||||
| @@ -407,6 +408,13 @@ func AddHandlers(h printers.PrintHandler) { | ||||
| 	_ = h.TableHandler(certificateSigningRequestColumnDefinitions, printCertificateSigningRequest) | ||||
| 	_ = 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{ | ||||
| 		{Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]}, | ||||
| 		{Name: "Holder", Type: "string", Description: coordinationv1.LeaseSpec{}.SwaggerDoc()["holderIdentity"]}, | ||||
| @@ -2095,6 +2103,30 @@ func printCertificateSigningRequestList(list *certificates.CertificateSigningReq | ||||
| 	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) { | ||||
| 	row := metav1.TableRow{ | ||||
| 		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 ( | ||||
| 	certificatesapiv1 "k8s.io/api/certificates/v1" | ||||
| 	certificatesapiv1alpha1 "k8s.io/api/certificates/v1alpha1" | ||||
| 	"k8s.io/apiserver/pkg/registry/generic" | ||||
| 	"k8s.io/apiserver/pkg/registry/rest" | ||||
| 	genericapiserver "k8s.io/apiserver/pkg/server" | ||||
| 	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/apis/certificates" | ||||
| 	"k8s.io/kubernetes/pkg/features" | ||||
| 	certificatestore "k8s.io/kubernetes/pkg/registry/certificates/certificates/storage" | ||||
| 	clustertrustbundlestore "k8s.io/kubernetes/pkg/registry/certificates/clustertrustbundle/storage" | ||||
| ) | ||||
|  | ||||
| type RESTStorageProvider struct{} | ||||
| @@ -40,17 +45,22 @@ func (p RESTStorageProvider) NewRESTStorage(apiResourceConfigSource serverstorag | ||||
| 		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 | ||||
| } | ||||
|  | ||||
| func (p RESTStorageProvider) v1Storage(apiResourceConfigSource serverstorage.APIResourceConfigSource, restOptionsGetter generic.RESTOptionsGetter) (map[string]rest.Storage, error) { | ||||
| 	storage := map[string]rest.Storage{} | ||||
|  | ||||
| 	// certificatesigningrequests | ||||
| 	if resource := "certificatesigningrequests"; apiResourceConfigSource.ResourceEnabled(certificatesapiv1.SchemeGroupVersion.WithResource(resource)) { | ||||
| 		csrStorage, csrStatusStorage, csrApprovalStorage, err := certificatestore.NewREST(restOptionsGetter) | ||||
| 		if err != nil { | ||||
| 			return storage, err | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		storage[resource] = csrStorage | ||||
| 		storage[resource+"/status"] = csrStatusStorage | ||||
| @@ -60,6 +70,24 @@ func (p RESTStorageProvider) v1Storage(apiResourceConfigSource serverstorage.API | ||||
| 	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 { | ||||
| 	return certificates.GroupName | ||||
| } | ||||
|   | ||||
| @@ -42,11 +42,11 @@ func NewEtcdStorageForResource(t *testing.T, resource schema.GroupResource) (*st | ||||
| 	completedConfig.APIResourceConfig = serverstorage.NewResourceConfig() | ||||
| 	factory, err := completedConfig.New() | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 		t.Fatalf("Error while making storage factory: %v", err) | ||||
| 	} | ||||
| 	resourceConfig, err := factory.NewConfig(resource) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 		t.Fatalf("Error while finding storage destination: %v", err) | ||||
| 	} | ||||
| 	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) { | ||||
| 		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 | ||||
| } | ||||
| @@ -585,6 +589,16 @@ func ClusterRoles() []rbacv1.ClusterRole { | ||||
| 		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) | ||||
| 	return roles | ||||
| } | ||||
| @@ -625,6 +639,11 @@ func ClusterRoleBindings() []rbacv1.ClusterRoleBinding { | ||||
| 		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) | ||||
|  | ||||
| 	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" | ||||
| 	batchv1beta1 "k8s.io/api/batch/v1beta1" | ||||
| 	certificatesv1 "k8s.io/api/certificates/v1" | ||||
| 	certificatesv1alpha1 "k8s.io/api/certificates/v1alpha1" | ||||
| 	certificatesv1beta1 "k8s.io/api/certificates/v1beta1" | ||||
| 	coordinationv1 "k8s.io/api/coordination/v1" | ||||
| 	coordinationv1beta1 "k8s.io/api/coordination/v1beta1" | ||||
| @@ -105,6 +106,7 @@ var groups = []runtime.SchemeBuilder{ | ||||
| 	batchv1.SchemeBuilder, | ||||
| 	certificatesv1.SchemeBuilder, | ||||
| 	certificatesv1beta1.SchemeBuilder, | ||||
| 	certificatesv1alpha1.SchemeBuilder, | ||||
| 	coordinationv1.SchemeBuilder, | ||||
| 	coordinationv1beta1.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/v1beta1 | ||||
| k8s.io/api/certificates/v1 | ||||
| k8s.io/api/certificates/v1alpha1 | ||||
| k8s.io/api/certificates/v1beta1 | ||||
| k8s.io/api/coordination/v1 | ||||
| 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/v1beta1 | ||||
| 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/coordination/v1 | ||||
| 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/certificates | ||||
| 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/coordination | ||||
| 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/certificates/v1 | ||||
| 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/fake | ||||
| 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/v1beta1 | ||||
| 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/coordination/v1 | ||||
| k8s.io/client-go/listers/coordination/v1beta1 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Taahir Ahmed
					Taahir Ahmed