Skip to content

Commit

Permalink
experimental adaptation of signer rotation test as fuzz target
Browse files Browse the repository at this point in the history
  • Loading branch information
benluddy authored and vrutkovs committed Apr 5, 2024
1 parent 6b235e9 commit f7264bc
Showing 1 changed file with 264 additions and 0 deletions.
264 changes: 264 additions & 0 deletions pkg/operator/certrotation/signer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import (
"bytes"
"context"
"fmt"
"math/rand"
"slices"
"strings"
"sync"
"testing"
"time"

Expand All @@ -13,7 +16,9 @@ import (

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
kubefake "k8s.io/client-go/kubernetes/fake"
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
corev1listers "k8s.io/client-go/listers/core/v1"
Expand Down Expand Up @@ -213,6 +218,7 @@ func (g *getter) Secrets(string) corev1client.SecretInterface {

type wrapped struct {
corev1client.SecretInterface
d *dispatcher
name string
t *testing.T
// the hooks are not invoked for every operation
Expand Down Expand Up @@ -568,3 +574,261 @@ func TestEnsureSigningCertKeyPair(t *testing.T) {
}
}
}

type dispatcher struct {
t *testing.T
source rand.Source
requests chan request
}

type request struct {
who string
what string
when chan<- struct{}
}

func (d *dispatcher) Sequence(who, what string) {
signal := make(chan struct{})
d.requests <- request{
who: who,
what: what,
when: signal,
}
<-signal
}

func (d *dispatcher) Join(who string) {
signal := make(chan struct{})
d.requests <- request{
who: who,
what: "JOIN",
when: signal,
}
}

func (d *dispatcher) Leave(who string) {
signal := make(chan struct{})
d.requests <- request{
who: who,
what: "LEAVE",
when: signal,
}
}

func (d *dispatcher) Stop() {
close(d.requests)
}

func (d *dispatcher) Run() {
members := make(map[string]struct{})
var waiting []request
rng := rand.New(d.source)

dispatch := func() {
slices.SortFunc(waiting, func(a, b request) int {
if a.who == b.who {
panic(fmt.Sprintf("two concurrent requests from same actor %q", a.who))
}
if a.who < b.who {
return -1
}
return 1
})
rng.Shuffle(len(waiting), func(i, j int) {
waiting[i], waiting[j] = waiting[j], waiting[i]
})
w := waiting[len(waiting)-1]
waiting = waiting[:len(waiting)-1]
d.t.Logf("dispatching %q by %q", w.what, w.who)
close(w.when)
}

for r := range d.requests {
switch r.what {
case "JOIN":
if _, ok := members[r.who]; ok {
d.t.Fatalf("double join by actor %q", r.who)
}
members[r.who] = struct{}{}
d.t.Logf("%q joined", r.who)
case "LEAVE":
if _, ok := members[r.who]; !ok {
d.t.Fatalf("double leave by actor %q", r.who)
}
delete(members, r.who)
d.t.Logf("%q left", r.who)
default:
waiting = append(waiting, r)
}

for len(waiting) > 0 && len(waiting) >= len(members) {
dispatch()
}
}

for range waiting {
dispatch()
}
}

type fakeSecretLister struct {
who string
dispatcher *dispatcher
tracker clienttesting.ObjectTracker
}

func (l *fakeSecretLister) List(selector labels.Selector) (ret []*corev1.Secret, err error) {
return l.Secrets("").List(selector)
}

func (l *fakeSecretLister) Secrets(namespace string) corev1listers.SecretNamespaceLister {
return &fakeSecretNamespaceLister{
who: l.who,
dispatcher: l.dispatcher,
tracker: l.tracker,
ns: namespace,
}
}

type fakeSecretNamespaceLister struct {
who string
dispatcher *dispatcher
tracker clienttesting.ObjectTracker
ns string
}

func (l *fakeSecretNamespaceLister) List(selector labels.Selector) (ret []*corev1.Secret, err error) {
obj, err := l.tracker.List(
schema.GroupVersionResource{Version: "v1", Resource: "secrets"},
schema.GroupVersionKind{Version: "v1", Kind: "Secret"},
l.ns,
)
var secrets []*corev1.Secret
if l, ok := obj.(*corev1.SecretList); ok {
for i := range l.Items {
secrets = append(secrets, &l.Items[i])
}
}
return secrets, err
}

func (l *fakeSecretNamespaceLister) Get(name string) (*corev1.Secret, error) {
l.dispatcher.Sequence(l.who, "before-lister-get")
obj, err := l.tracker.Get(schema.GroupVersionResource{Version: "v1", Resource: "secrets"}, l.ns, name)
l.dispatcher.Sequence(l.who, "after-lister-get")
if secret, ok := obj.(*corev1.Secret); ok {
return secret, err
}
return nil, err
}

func FuzzEnsureSigningCertKeyPair(f *testing.F) {
const (
WorkerCount = 3
SecretNamespace, SecretName = "ns", "test-signer"
)
// represents a secret that was created before 4.7 and
// hasn't been updated until now (upgrade to 4.15)
existing := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: SecretNamespace,
Name: SecretName,
ResourceVersion: "10",
},
Type: "SecretTypeTLS",
Data: map[string][]byte{"tls.crt": {}, "tls.key": {}},
}
if err := setSigningCertKeyPairSecret(existing, 24*time.Hour); err != nil {
f.Fatal(err)
}
// give it a second so we have a unique signer name,
// and also unique not-after, and not-before values
<-time.After(2 * time.Second)

f.Fuzz(func(t *testing.T, seed int64) {
d := &dispatcher{
t: t,
source: rand.NewSource(seed),
requests: make(chan request, WorkerCount),
}
go d.Run()
defer d.Stop()

existing = existing.DeepCopy()

// get the original crt and key bytes to compare later
tlsCertWant, ok := existing.Data["tls.crt"]
if !ok || len(tlsCertWant) == 0 {
t.Fatalf("missing data in 'tls.crt' key of Data: %#v", existing.Data)
}
tlsKeyWant, ok := existing.Data["tls.key"]
if !ok || len(tlsKeyWant) == 0 {
t.Fatalf("missing data in 'tls.key' key of Data: %#v", existing.Data)
}

secretWant := existing.DeepCopy()

clientset := kubefake.NewSimpleClientset(existing)

options := events.RecommendedClusterSingletonCorrelatorOptions()
client := clientset.CoreV1().Secrets(SecretNamespace)

var wg sync.WaitGroup
for i := 1; i <= WorkerCount; i++ {
controllerName := fmt.Sprintf("controller-%d", i)
wg.Add(1)
d.Join(controllerName)

go func(controllerName string) {
defer func() {
d.Leave(controllerName)
wg.Done()
}()

recorder := events.NewKubeRecorderWithOptions(clientset.CoreV1().Events(SecretNamespace), options, "operator", &corev1.ObjectReference{Name: controllerName, Namespace: SecretNamespace})
wrapped := &wrapped{SecretInterface: client, name: controllerName, t: t, d: d}
getter := &getter{w: wrapped}
ctrl := &RotatedSigningCASecret{
Namespace: SecretNamespace,
Name: SecretName,
Validity: 24 * time.Hour,
Refresh: 12 * time.Hour,
Client: getter,
Lister: &fakeSecretLister{
who: controllerName,
dispatcher: d,
tracker: clientset.Tracker(),
},
AdditionalAnnotations: AdditionalAnnotations{JiraComponent: "test"},
Owner: &metav1.OwnerReference{Name: "operator"},
EventRecorder: recorder,
}

d.Sequence(controllerName, "begin")
_, _, err := ctrl.EnsureSigningCertKeyPair(context.TODO())
if err != nil {
t.Logf("error from %s: %v", controllerName, err)
}
}(controllerName)
}

wg.Wait()
t.Log("controllers done")
// controllers are done, we don't expect the signer to change
secretGot, err := client.Get(context.TODO(), SecretName, metav1.GetOptions{})
if err != nil {
t.Errorf("unexpected error: %v", err)
return
}
if tlsCertGot, ok := secretGot.Data["tls.crt"]; !ok || !bytes.Equal(tlsCertWant, tlsCertGot) {
t.Errorf("the signer cert has mutated unexpectedly")
}
if tlsKeyGot, ok := secretGot.Data["tls.key"]; !ok || !bytes.Equal(tlsKeyWant, tlsKeyGot) {
t.Errorf("the signer cert has mutated unexpectedly")
}
if got, exists := secretGot.Annotations["openshift.io/owning-component"]; !exists || got != "test" {
t.Errorf("owner annotation is missing: %#v", secretGot.Annotations)
}
t.Logf("diff: %s", cmp.Diff(secretWant, secretGot))
})
}

0 comments on commit f7264bc

Please sign in to comment.