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:
Taahir Ahmed 2022-11-04 12:20:25 -07:00
parent 742316ee21
commit 6a75e7c40c
30 changed files with 1979 additions and 7 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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

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

View File

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

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

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

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

View File

@ -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
View File

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