trustbundles: add a new kube-apiserver-serving signer

This commit is contained in:
Stanislav Láznička
2024-09-03 12:48:05 +02:00
parent 09e5e6269a
commit a4b83e77d9
10 changed files with 1179 additions and 54 deletions

View File

@@ -0,0 +1,78 @@
/*
Copyright 2024 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 clustertrustbundlepublisher
import (
"errors"
"strconv"
"sync"
"time"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/component-base/metrics"
"k8s.io/component-base/metrics/legacyregistry"
)
// clustertrustbundlePublisher - subsystem name used by clustertrustbundle_publisher
const (
clustertrustbundlePublisher = "clustertrustbundle_publisher"
)
var (
syncCounter = metrics.NewCounterVec(
&metrics.CounterOpts{
Subsystem: clustertrustbundlePublisher,
Name: "sync_total",
Help: "Number of syncs that occurred in cluster trust bundle publisher.",
StabilityLevel: metrics.ALPHA,
},
[]string{"code"},
)
syncLatency = metrics.NewHistogramVec(
&metrics.HistogramOpts{
Subsystem: clustertrustbundlePublisher,
Name: "sync_duration_seconds",
Help: "The time it took to sync a cluster trust bundle.",
Buckets: metrics.ExponentialBuckets(0.001, 2, 15),
StabilityLevel: metrics.ALPHA,
},
[]string{"code"},
)
)
func recordMetrics(start time.Time, err error) {
code := "500"
if err == nil {
code = "200"
} else {
var statusErr apierrors.APIStatus
if errors.As(err, &statusErr) && statusErr.Status().Code != 0 {
code = strconv.Itoa(int(statusErr.Status().Code))
}
}
syncLatency.WithLabelValues(code).Observe(time.Since(start).Seconds())
syncCounter.WithLabelValues(code).Inc()
}
var once sync.Once
func registerMetrics() {
once.Do(func() {
legacyregistry.MustRegister(syncCounter)
legacyregistry.MustRegister(syncLatency)
})
}

View File

@@ -0,0 +1,110 @@
/*
Copyright 2024 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 clustertrustbundlepublisher
import (
"errors"
"fmt"
"strings"
"testing"
"time"
certificatesv1alpha1 "k8s.io/api/certificates/v1alpha1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/component-base/metrics/legacyregistry"
"k8s.io/component-base/metrics/testutil"
)
func TestSyncCounter(t *testing.T) {
testCases := []struct {
desc string
err error
metrics []string
want string
}{
{
desc: "nil error",
err: nil,
metrics: []string{
"clustertrustbundle_publisher_sync_total",
},
want: `
# HELP clustertrustbundle_publisher_sync_total [ALPHA] Number of syncs that occurred in cluster trust bundle publisher.
# TYPE clustertrustbundle_publisher_sync_total counter
clustertrustbundle_publisher_sync_total{code="200"} 1
`,
},
{
desc: "kube api error",
err: apierrors.NewNotFound(certificatesv1alpha1.Resource("clustertrustbundle"), "test.test:testSigner:something"),
metrics: []string{
"clustertrustbundle_publisher_sync_total",
},
want: `
# HELP clustertrustbundle_publisher_sync_total [ALPHA] Number of syncs that occurred in cluster trust bundle publisher.
# TYPE clustertrustbundle_publisher_sync_total counter
clustertrustbundle_publisher_sync_total{code="404"} 1
`,
},
{
desc: "nested kube api error",
err: fmt.Errorf("oh noes: %w", apierrors.NewBadRequest("bad request!")),
metrics: []string{
"clustertrustbundle_publisher_sync_total",
},
want: `
# HELP clustertrustbundle_publisher_sync_total [ALPHA] Number of syncs that occurred in cluster trust bundle publisher.
# TYPE clustertrustbundle_publisher_sync_total counter
clustertrustbundle_publisher_sync_total{code="400"} 1
`,
},
{
desc: "kube api error without code",
err: &apierrors.StatusError{},
metrics: []string{
"clustertrustbundle_publisher_sync_total",
},
want: `
# HELP clustertrustbundle_publisher_sync_total [ALPHA] Number of syncs that occurred in cluster trust bundle publisher.
# TYPE clustertrustbundle_publisher_sync_total counter
clustertrustbundle_publisher_sync_total{code="500"} 1
`,
},
{
desc: "general error",
err: errors.New("test"),
metrics: []string{
"clustertrustbundle_publisher_sync_total",
},
want: `
# HELP clustertrustbundle_publisher_sync_total [ALPHA] Number of syncs that occurred in cluster trust bundle publisher.
# TYPE clustertrustbundle_publisher_sync_total counter
clustertrustbundle_publisher_sync_total{code="500"} 1
`,
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
recordMetrics(time.Now(), tc.err)
defer syncCounter.Reset()
if err := testutil.GatherAndCompare(legacyregistry.DefaultGatherer, strings.NewReader(tc.want), tc.metrics...); err != nil {
t.Fatal(err)
}
})
}
}

View File

@@ -0,0 +1,229 @@
/*
Copyright 2024 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 clustertrustbundlepublisher
import (
"context"
"crypto/sha256"
"fmt"
"strings"
"time"
certificatesv1alpha1 "k8s.io/api/certificates/v1alpha1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/server/dynamiccertificates"
certinformers "k8s.io/client-go/informers/certificates/v1alpha1"
clientset "k8s.io/client-go/kubernetes"
certlisters "k8s.io/client-go/listers/certificates/v1alpha1"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue"
"k8s.io/klog/v2"
)
func init() {
registerMetrics()
}
type ClusterTrustBundlePublisher struct {
signerName string
ca dynamiccertificates.CAContentProvider
client clientset.Interface
ctbInformer cache.SharedIndexInformer
ctbLister certlisters.ClusterTrustBundleLister
ctbListerSynced cache.InformerSynced
queue workqueue.TypedRateLimitingInterface[string]
}
type caContentListener func()
func (f caContentListener) Enqueue() {
f()
}
// NewClusterTrustBundlePublisher creates and maintains a cluster trust bundle object
// for a signer named `signerName`. The cluster trust bundle object contains the
// CA from the `caProvider` in its .spec.TrustBundle.
func NewClusterTrustBundlePublisher(
signerName string,
caProvider dynamiccertificates.CAContentProvider,
kubeClient clientset.Interface,
) (*ClusterTrustBundlePublisher, error) {
if len(signerName) == 0 {
return nil, fmt.Errorf("signerName cannot be empty")
}
p := &ClusterTrustBundlePublisher{
signerName: signerName,
ca: caProvider,
client: kubeClient,
queue: workqueue.NewTypedRateLimitingQueueWithConfig(
workqueue.DefaultTypedControllerRateLimiter[string](),
workqueue.TypedRateLimitingQueueConfig[string]{
Name: "ca_cert_publisher_cluster_trust_bundles",
},
),
}
p.ctbInformer = setupSignerNameFilteredCTBInformer(p.client, p.signerName)
p.ctbLister = certlisters.NewClusterTrustBundleLister(p.ctbInformer.GetIndexer())
p.ctbListerSynced = p.ctbInformer.HasSynced
_, err := p.ctbInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
p.queue.Add("")
},
UpdateFunc: func(_, _ interface{}) {
p.queue.Add("")
},
DeleteFunc: func(_ interface{}) {
p.queue.Add("")
},
})
if err != nil {
return nil, fmt.Errorf("failed to register ClusterTrustBundle event handler: %w", err)
}
p.ca.AddListener(p.caContentChangedListener())
return p, nil
}
func (p *ClusterTrustBundlePublisher) caContentChangedListener() dynamiccertificates.Listener {
return caContentListener(func() {
p.queue.Add("")
})
}
func (p *ClusterTrustBundlePublisher) Run(ctx context.Context) {
defer utilruntime.HandleCrash()
defer p.queue.ShutDown()
logger := klog.FromContext(ctx)
logger.Info("Starting ClusterTrustBundle CA cert publisher controller")
defer logger.Info("Shutting down ClusterTrustBundle CA cert publisher controller")
go p.ctbInformer.Run(ctx.Done())
if !cache.WaitForNamedCacheSync("cluster trust bundle", ctx.Done(), p.ctbListerSynced) {
return
}
// init the signer syncer
p.queue.Add("")
go wait.UntilWithContext(ctx, p.runWorker(), time.Second)
<-ctx.Done()
}
func (p *ClusterTrustBundlePublisher) runWorker() func(context.Context) {
return func(ctx context.Context) {
for p.processNextWorkItem(ctx) {
}
}
}
// processNextWorkItem deals with one key off the queue. It returns false when
// it's time to quit.
func (p *ClusterTrustBundlePublisher) processNextWorkItem(ctx context.Context) bool {
key, quit := p.queue.Get()
if quit {
return false
}
defer p.queue.Done(key)
if err := p.syncClusterTrustBundle(ctx); err != nil {
utilruntime.HandleError(fmt.Errorf("syncing %q failed: %w", key, err))
p.queue.AddRateLimited(key)
return true
}
p.queue.Forget(key)
return true
}
func (p *ClusterTrustBundlePublisher) syncClusterTrustBundle(ctx context.Context) (err error) {
startTime := time.Now()
defer func() {
recordMetrics(startTime, err)
klog.FromContext(ctx).V(4).Info("Finished syncing ClusterTrustBundle", "signerName", p.signerName, "elapsedTime", time.Since(startTime))
}()
caBundle := string(p.ca.CurrentCABundleContent())
bundleName := constructBundleName(p.signerName, []byte(caBundle))
bundle, err := p.ctbLister.Get(bundleName)
if apierrors.IsNotFound(err) {
_, err = p.client.CertificatesV1alpha1().ClusterTrustBundles().Create(ctx, &certificatesv1alpha1.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{
Name: bundleName,
},
Spec: certificatesv1alpha1.ClusterTrustBundleSpec{
SignerName: p.signerName,
TrustBundle: caBundle,
},
}, metav1.CreateOptions{})
} else if err == nil && bundle.Spec.TrustBundle != caBundle {
bundle = bundle.DeepCopy()
bundle.Spec.TrustBundle = caBundle
_, err = p.client.CertificatesV1alpha1().ClusterTrustBundles().Update(ctx, bundle, metav1.UpdateOptions{})
}
if err != nil {
return err
}
signerTrustBundles, err := p.ctbLister.List(labels.Everything())
if err != nil {
return err
}
// keep the deletion error to be returned in the end in order to retrigger the reconciliation loop
var deletionError error
for _, bundleObject := range signerTrustBundles {
if bundleObject.Name == bundleName {
continue
}
if err := p.client.CertificatesV1alpha1().ClusterTrustBundles().Delete(ctx, bundleObject.Name, metav1.DeleteOptions{}); err != nil && !apierrors.IsNotFound(err) {
klog.FromContext(ctx).Error(err, "failed to remove a cluster trust bundle", "bundleName", bundleObject.Name)
deletionError = err
}
}
return deletionError
}
func setupSignerNameFilteredCTBInformer(client clientset.Interface, signerName string) cache.SharedIndexInformer {
return certinformers.NewFilteredClusterTrustBundleInformer(client, 0, cache.Indexers{},
func(options *metav1.ListOptions) {
options.FieldSelector = fields.OneTermEqualSelector("spec.signerName", signerName).String()
})
}
func constructBundleName(signerName string, bundleBytes []byte) string {
namePrefix := strings.ReplaceAll(signerName, "/", ":") + ":"
bundleHash := sha256.Sum256(bundleBytes)
return fmt.Sprintf("%s%x", namePrefix, bundleHash[:12])
}

View File

@@ -0,0 +1,362 @@
/*
Copyright 2024 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 clustertrustbundlepublisher
import (
"crypto/ecdsa"
"crypto/elliptic"
cryptorand "crypto/rand"
"testing"
certificatesv1alpha1 "k8s.io/api/certificates/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/server/dynamiccertificates"
"k8s.io/client-go/kubernetes/fake"
clienttesting "k8s.io/client-go/testing"
"k8s.io/client-go/tools/cache"
certutil "k8s.io/client-go/util/cert"
"k8s.io/kubernetes/test/utils/ktesting"
)
const testSignerName = "test.test/testSigner"
func TestCTBPublisherSync(t *testing.T) {
checkCreatedTestSignerBundle := func(t *testing.T, actions []clienttesting.Action) {
filteredActions := filterOutListWatch(actions)
if len(filteredActions) != 1 {
t.Fatalf("expected 1 action, got %v", filteredActions)
}
createAction := expectAction[clienttesting.CreateAction](t, filteredActions[0], "create")
ctb, ok := createAction.GetObject().(*certificatesv1alpha1.ClusterTrustBundle)
if !ok {
t.Fatalf("expected ClusterTrustBundle create, got %v", createAction.GetObject())
}
if ctb.Spec.SignerName != testSignerName {
t.Fatalf("expected signer name %q, got %q", testSignerName, ctb.Spec.SignerName)
}
}
checkUpdatedTestSignerBundle := func(expectedCABytes []byte) func(t *testing.T, actions []clienttesting.Action) {
return func(t *testing.T, actions []clienttesting.Action) {
filteredActions := filterOutListWatch(actions)
if len(filteredActions) != 1 {
t.Fatalf("expected 1 action, got %v", filteredActions)
}
updateAction := expectAction[clienttesting.UpdateAction](t, filteredActions[0], "update")
ctb, ok := updateAction.GetObject().(*certificatesv1alpha1.ClusterTrustBundle)
if !ok {
t.Fatalf("expected ClusterTrustBundle update, got %v", updateAction.GetObject())
}
if ctb.Spec.SignerName != testSignerName {
t.Fatalf("expected signer name %q, got %q", testSignerName, ctb.Spec.SignerName)
}
if ctb.Spec.TrustBundle != string(expectedCABytes) {
t.Fatalf("expected trust bundle payload:\n%s\n, got %q", expectedCABytes, ctb.Spec.TrustBundle)
}
}
}
checkDeletedTestSignerBundle := func(name string) func(t *testing.T, actions []clienttesting.Action) {
return func(t *testing.T, actions []clienttesting.Action) {
filteredActions := filterOutListWatch(actions)
if len(filteredActions) != 1 {
t.Fatalf("expected 1 action, got %v", filteredActions)
}
deleteAction := expectAction[clienttesting.DeleteAction](t, filteredActions[0], "delete")
if actionName := deleteAction.GetName(); actionName != name {
t.Fatalf("expected deleted CTB name %q, got %q", name, actionName)
}
}
}
testCAProvider := testingCABundlleProvider(t)
testBundleName := constructBundleName(testSignerName, testCAProvider.CurrentCABundleContent())
for _, tt := range []struct {
name string
existingCTBs []runtime.Object
expectActions func(t *testing.T, actions []clienttesting.Action)
wantErr bool
}{
{
name: "no CTBs exist",
expectActions: checkCreatedTestSignerBundle,
},
{
name: "no CTBs for the current signer exist",
existingCTBs: []runtime.Object{
&certificatesv1alpha1.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{
Name: "nosigner",
},
Spec: certificatesv1alpha1.ClusterTrustBundleSpec{
TrustBundle: "somedatahere",
},
},
&certificatesv1alpha1.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{
Name: "signer:one",
},
Spec: certificatesv1alpha1.ClusterTrustBundleSpec{
SignerName: "signer",
TrustBundle: "signerdata",
},
},
},
expectActions: checkCreatedTestSignerBundle,
},
{
name: "CTB for the signer exists with different content",
existingCTBs: []runtime.Object{
&certificatesv1alpha1.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{
Name: testBundleName,
},
Spec: certificatesv1alpha1.ClusterTrustBundleSpec{
SignerName: testSignerName,
TrustBundle: "olddata",
},
},
},
expectActions: checkUpdatedTestSignerBundle(testCAProvider.CurrentCABundleContent()),
},
{
name: "multiple CTBs for the signer",
existingCTBs: []runtime.Object{
&certificatesv1alpha1.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{
Name: testBundleName,
},
Spec: certificatesv1alpha1.ClusterTrustBundleSpec{
SignerName: testSignerName,
TrustBundle: string(testCAProvider.CurrentCABundleContent()),
},
},
&certificatesv1alpha1.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{
Name: "test.test/testSigner:name2",
},
Spec: certificatesv1alpha1.ClusterTrustBundleSpec{
SignerName: testSignerName,
TrustBundle: string(testCAProvider.CurrentCABundleContent()),
},
},
},
expectActions: checkDeletedTestSignerBundle("test.test/testSigner:name2"),
},
{
name: "multiple CTBs for the signer - the one with the proper name needs changing",
existingCTBs: []runtime.Object{
&certificatesv1alpha1.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{
Name: testBundleName,
},
Spec: certificatesv1alpha1.ClusterTrustBundleSpec{
SignerName: testSignerName,
TrustBundle: "olddata",
},
},
&certificatesv1alpha1.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{
Name: "test.test/testSigner:name2",
},
Spec: certificatesv1alpha1.ClusterTrustBundleSpec{
SignerName: testSignerName,
TrustBundle: string(testCAProvider.CurrentCABundleContent()),
},
},
},
expectActions: func(t *testing.T, actions []clienttesting.Action) {
filteredActions := filterOutListWatch(actions)
if len(filteredActions) != 2 {
t.Fatalf("expected 2 actions, got %v", filteredActions)
}
checkUpdatedTestSignerBundle(testCAProvider.CurrentCABundleContent())(t, filteredActions[:1])
checkDeletedTestSignerBundle("test.test/testSigner:name2")(t, filteredActions[1:])
},
},
{
name: "another CTB with a different name exists for the signer",
existingCTBs: []runtime.Object{
&certificatesv1alpha1.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{
Name: "test.test/testSigner:preexisting",
},
Spec: certificatesv1alpha1.ClusterTrustBundleSpec{
SignerName: testSignerName,
TrustBundle: string(testCAProvider.CurrentCABundleContent()),
},
},
},
expectActions: func(t *testing.T, actions []clienttesting.Action) {
filteredActions := filterOutListWatch(actions)
if len(filteredActions) != 2 {
t.Fatalf("expected 2 actions, got %v", filteredActions)
}
checkCreatedTestSignerBundle(t, filteredActions[:1])
checkDeletedTestSignerBundle("test.test/testSigner:preexisting")(t, filteredActions[1:])
},
},
{
name: "CTB at the correct state - noop",
existingCTBs: []runtime.Object{
&certificatesv1alpha1.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{
Name: "nosigner",
},
Spec: certificatesv1alpha1.ClusterTrustBundleSpec{
TrustBundle: "somedatahere",
},
},
&certificatesv1alpha1.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{
Name: "signer:one",
},
Spec: certificatesv1alpha1.ClusterTrustBundleSpec{
SignerName: "signer",
TrustBundle: "signerdata",
},
},
&certificatesv1alpha1.ClusterTrustBundle{
ObjectMeta: metav1.ObjectMeta{
Name: testBundleName,
},
Spec: certificatesv1alpha1.ClusterTrustBundleSpec{
SignerName: testSignerName,
TrustBundle: string(testCAProvider.CurrentCABundleContent()),
},
},
},
expectActions: func(t *testing.T, actions []clienttesting.Action) {
actions = filterOutListWatch(actions)
if len(actions) != 0 {
t.Fatalf("expected no actions, got %v", actions)
}
},
},
} {
t.Run(tt.name, func(t *testing.T) {
testCtx := ktesting.Init(t)
fakeClient := fakeKubeClientSetWithCTBList(t, testSignerName, tt.existingCTBs...)
p, err := NewClusterTrustBundlePublisher(testSignerName, testCAProvider, fakeClient)
if err != nil {
t.Fatalf("failed to set up a new cluster trust bundle publisher: %v", err)
}
go p.ctbInformer.Run(testCtx.Done())
if !cache.WaitForCacheSync(testCtx.Done(), p.ctbInformer.HasSynced) {
t.Fatal("timed out waiting for informer to sync")
}
if err := p.syncClusterTrustBundle(testCtx); (err != nil) != tt.wantErr {
t.Errorf("syncClusterTrustBundle() error = %v, wantErr %v", err, tt.wantErr)
}
tt.expectActions(t, fakeClient.Actions())
})
}
}
func fakeKubeClientSetWithCTBList(t *testing.T, signerName string, ctbs ...runtime.Object) *fake.Clientset {
fakeClient := fake.NewSimpleClientset(ctbs...)
fakeClient.PrependReactor("list", "clustertrustbundles", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
listAction, ok := action.(clienttesting.ListAction)
if !ok {
t.Fatalf("expected list action, got %v", action)
}
// fakeClient does not implement field selector, we have to do it manually
listRestrictions := listAction.GetListRestrictions()
if listRestrictions.Fields == nil || listRestrictions.Fields.String() != ("spec.signerName="+signerName) {
return false, nil, nil
}
retList := &certificatesv1alpha1.ClusterTrustBundleList{}
for _, ctb := range ctbs {
ctbObj, ok := ctb.(*certificatesv1alpha1.ClusterTrustBundle)
if !ok {
continue
}
if ctbObj.Spec.SignerName == testSignerName {
retList.Items = append(retList.Items, *ctbObj)
}
}
return true, retList, nil
})
return fakeClient
}
func expectAction[A clienttesting.Action](t *testing.T, action clienttesting.Action, verb string) A {
if action.GetVerb() != verb {
t.Fatalf("expected action with verb %q, got %q", verb, action.GetVerb())
}
retAction, ok := action.(A)
if !ok {
t.Fatalf("expected %T action, got %v", *new(A), action)
}
return retAction
}
func filterOutListWatch(actions []clienttesting.Action) []clienttesting.Action {
var filtered []clienttesting.Action
for _, a := range actions {
if a.Matches("list", "clustertrustbundles") || a.Matches("watch", "clustertrustbundles") {
continue
}
filtered = append(filtered, a)
}
return filtered
}
func testingCABundlleProvider(t *testing.T) dynamiccertificates.CAContentProvider {
key, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader)
if err != nil {
t.Fatalf("failed to create a private key: %v", err)
}
caCert, err := certutil.NewSelfSignedCACert(certutil.Config{CommonName: "test-ca"}, key)
if err != nil {
t.Fatalf("failed to create a self-signed CA cert: %v", err)
}
caPEM, err := certutil.EncodeCertificates(caCert)
if err != nil {
t.Fatalf("failed to PEM-encode cert: %v", err)
}
caProvider, err := dynamiccertificates.NewStaticCAContent("testca", caPEM)
if err != nil {
t.Fatalf("failed to create a static CA provider: %v", err)
}
return caProvider
}