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