ContainerImage.Pinniped/internal/controller/apicerts/certs_manager_test.go
Matt Moyer 1b9a70d089
Switch back to an exec-based approach to grab the controller-manager CA. (#65)
This switches us back to an approach where we use the Pod "exec" API to grab the keys we need, rather than forcing our code to run on the control plane node. It will help us fail gracefully (or dynamically switch to alternate implementations) when the cluster is not self-hosted.

Signed-off-by: Matt Moyer <moyerm@vmware.com>
Co-authored-by: Ryan Richard <richardry@vmware.com>
2020-08-19 13:21:07 -05:00

331 lines
12 KiB
Go

/*
Copyright 2020 VMware, Inc.
SPDX-License-Identifier: Apache-2.0
*/
package apicerts
import (
"context"
"errors"
"testing"
"time"
"github.com/sclevine/spec"
"github.com/sclevine/spec/report"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
kubeinformers "k8s.io/client-go/informers"
kubernetesfake "k8s.io/client-go/kubernetes/fake"
coretesting "k8s.io/client-go/testing"
apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
aggregatorfake "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/fake"
"github.com/suzerain-io/controller-go"
"github.com/suzerain-io/placeholder-name/internal/testutil"
placeholderv1alpha1 "github.com/suzerain-io/placeholder-name/kubernetes/1.19/api/apis/placeholder/v1alpha1"
)
func TestManagerControllerOptions(t *testing.T) {
spec.Run(t, "options", func(t *testing.T, when spec.G, it spec.S) {
const installedInNamespace = "some-namespace"
var r *require.Assertions
var observableWithInformerOption *testutil.ObservableWithInformerOption
var observableWithInitialEventOption *testutil.ObservableWithInitialEventOption
var secretsInformerFilter controller.Filter
it.Before(func() {
r = require.New(t)
observableWithInformerOption = testutil.NewObservableWithInformerOption()
observableWithInitialEventOption = testutil.NewObservableWithInitialEventOption()
secretsInformer := kubeinformers.NewSharedInformerFactory(nil, 0).Core().V1().Secrets()
_ = NewCertsManagerController(
installedInNamespace,
nil,
nil,
secretsInformer,
observableWithInformerOption.WithInformer, // make it possible to observe the behavior of the Filters
observableWithInitialEventOption.WithInitialEvent, // make it possible to observe the behavior of the initial event
)
secretsInformerFilter = observableWithInformerOption.GetFilterForInformer(secretsInformer)
})
when("watching Secret objects", func() {
var subject controller.Filter
var target, wrongNamespace, wrongName, unrelated *corev1.Secret
it.Before(func() {
subject = secretsInformerFilter
target = &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "api-serving-cert", Namespace: installedInNamespace}}
wrongNamespace = &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "api-serving-cert", Namespace: "wrong-namespace"}}
wrongName = &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "wrong-name", Namespace: installedInNamespace}}
unrelated = &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "wrong-name", Namespace: "wrong-namespace"}}
})
when("the target Secret changes", func() {
it("returns true to trigger the sync method", func() {
r.True(subject.Add(target))
r.True(subject.Update(target, unrelated))
r.True(subject.Update(unrelated, target))
r.True(subject.Delete(target))
})
})
when("a Secret from another namespace changes", func() {
it("returns false to avoid triggering the sync method", func() {
r.False(subject.Add(wrongNamespace))
r.False(subject.Update(wrongNamespace, unrelated))
r.False(subject.Update(unrelated, wrongNamespace))
r.False(subject.Delete(wrongNamespace))
})
})
when("a Secret with a different name changes", func() {
it("returns false to avoid triggering the sync method", func() {
r.False(subject.Add(wrongName))
r.False(subject.Update(wrongName, unrelated))
r.False(subject.Update(unrelated, wrongName))
r.False(subject.Delete(wrongName))
})
})
when("a Secret with a different name and a different namespace changes", func() {
it("returns false to avoid triggering the sync method", func() {
r.False(subject.Add(unrelated))
r.False(subject.Update(unrelated, unrelated))
r.False(subject.Delete(unrelated))
})
})
})
when("starting up", func() {
it("asks for an initial event because the Secret may not exist yet and it needs to run anyway", func() {
r.Equal(controller.Key{
Namespace: installedInNamespace,
Name: "api-serving-cert",
}, observableWithInitialEventOption.GetInitialEventKey())
})
})
}, spec.Parallel(), spec.Report(report.Terminal{}))
}
func TestManagerControllerSync(t *testing.T) {
spec.Run(t, "Sync", func(t *testing.T, when spec.G, it spec.S) {
const installedInNamespace = "some-namespace"
var r *require.Assertions
var subject controller.Controller
var kubeAPIClient *kubernetesfake.Clientset
var aggregatorAPIClient *aggregatorfake.Clientset
var kubeInformerClient *kubernetesfake.Clientset
var kubeInformers kubeinformers.SharedInformerFactory
var timeoutContext context.Context
var timeoutContextCancel context.CancelFunc
var syncContext *controller.Context
// Defer starting the informers until the last possible moment so that the
// nested Before's can keep adding things to the informer caches.
var startInformersAndController = func() {
// Set this at the last second to allow for injection of server override.
subject = NewCertsManagerController(
installedInNamespace,
kubeAPIClient,
aggregatorAPIClient,
kubeInformers.Core().V1().Secrets(),
controller.WithInformer,
controller.WithInitialEvent,
)
// Set this at the last second to support calling subject.Name().
syncContext = &controller.Context{
Context: timeoutContext,
Name: subject.Name(),
Key: controller.Key{
Namespace: installedInNamespace,
Name: "api-serving-cert",
},
}
// Must start informers before calling TestRunSynchronously()
kubeInformers.Start(timeoutContext.Done())
controller.TestRunSynchronously(t, subject)
}
it.Before(func() {
r = require.New(t)
timeoutContext, timeoutContextCancel = context.WithTimeout(context.Background(), time.Second*3)
kubeInformerClient = kubernetesfake.NewSimpleClientset()
kubeInformers = kubeinformers.NewSharedInformerFactory(kubeInformerClient, 0)
kubeAPIClient = kubernetesfake.NewSimpleClientset()
aggregatorAPIClient = aggregatorfake.NewSimpleClientset()
})
it.After(func() {
timeoutContextCancel()
})
when("there is not yet an api-serving-cert Secret in the installation namespace or it was deleted", func() {
it.Before(func() {
unrelatedSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "some other secret",
Namespace: installedInNamespace,
},
}
err := kubeInformerClient.Tracker().Add(unrelatedSecret)
r.NoError(err)
})
when("the APIService exists", func() {
it.Before(func() {
apiService := &apiregistrationv1.APIService{
ObjectMeta: metav1.ObjectMeta{
Name: placeholderv1alpha1.SchemeGroupVersion.Version + "." + placeholderv1alpha1.GroupName,
},
Spec: apiregistrationv1.APIServiceSpec{
CABundle: nil,
VersionPriority: 1234,
},
}
err := aggregatorAPIClient.Tracker().Add(apiService)
r.NoError(err)
})
it("creates the api-serving-cert Secret and updates the APIService's ca bundle", func() {
startInformersAndController()
err := controller.TestSync(t, subject, *syncContext)
r.NoError(err)
// Check all the relevant fields from the create Secret action
r.Len(kubeAPIClient.Actions(), 1)
actualAction := kubeAPIClient.Actions()[0].(coretesting.CreateActionImpl)
r.Equal(schema.GroupVersionResource{Group: "", Version: "v1", Resource: "secrets"}, actualAction.GetResource())
r.Equal(installedInNamespace, actualAction.GetNamespace())
actualSecret := actualAction.GetObject().(*corev1.Secret)
r.Equal("api-serving-cert", actualSecret.Name)
r.Equal(installedInNamespace, actualSecret.Namespace)
actualCACert := actualSecret.StringData["caCertificate"]
actualPrivateKey := actualSecret.StringData["tlsPrivateKey"]
actualCertChain := actualSecret.StringData["tlsCertificateChain"]
r.NotEmpty(actualCACert)
r.NotEmpty(actualPrivateKey)
r.NotEmpty(actualCertChain)
// Validate the created cert using the CA, and also validate the cert's hostname
validCert := testutil.ValidateCertificate(t, actualCACert, actualCertChain)
validCert.RequireDNSName("placeholder-name-api." + installedInNamespace + ".svc")
validCert.RequireLifetime(time.Now(), time.Now().Add(24*365*time.Hour), 2*time.Minute)
validCert.RequireMatchesPrivateKey(actualPrivateKey)
// Make sure we updated the APIService caBundle and left it otherwise unchanged
r.Len(aggregatorAPIClient.Actions(), 2)
r.Equal("get", aggregatorAPIClient.Actions()[0].GetVerb())
expectedAPIServiceName := placeholderv1alpha1.SchemeGroupVersion.Version + "." + placeholderv1alpha1.GroupName
expectedUpdateAction := coretesting.NewUpdateAction(
schema.GroupVersionResource{
Group: apiregistrationv1.GroupName,
Version: "v1",
Resource: "apiservices",
},
"",
&apiregistrationv1.APIService{
ObjectMeta: metav1.ObjectMeta{
Name: expectedAPIServiceName,
Namespace: "",
},
Spec: apiregistrationv1.APIServiceSpec{
VersionPriority: 1234, // only the CABundle is updated, this other field is left unchanged
CABundle: []byte(actualCACert),
},
},
)
r.Equal(expectedUpdateAction, aggregatorAPIClient.Actions()[1])
})
when("updating the APIService fails", func() {
it.Before(func() {
aggregatorAPIClient.PrependReactor(
"update",
"apiservices",
func(_ coretesting.Action) (bool, runtime.Object, error) {
return true, nil, errors.New("update failed")
},
)
})
it("returns the update error", func() {
startInformersAndController()
err := controller.TestSync(t, subject, *syncContext)
r.EqualError(err, "could not update the API service: could not update API service: update failed")
})
})
})
when("the APIService does not exist", func() {
it.Before(func() {
unrelatedAPIService := &apiregistrationv1.APIService{
ObjectMeta: metav1.ObjectMeta{Name: "some other api service"},
Spec: apiregistrationv1.APIServiceSpec{},
}
err := aggregatorAPIClient.Tracker().Add(unrelatedAPIService)
r.NoError(err)
})
it("returns an error", func() {
startInformersAndController()
err := controller.TestSync(t, subject, *syncContext)
r.Error(err)
r.Regexp("could not get existing version of API service: .* not found", err.Error())
})
})
when("creating the Secret fails", func() {
it.Before(func() {
kubeAPIClient.PrependReactor(
"create",
"secrets",
func(_ coretesting.Action) (bool, runtime.Object, error) {
return true, nil, errors.New("create failed")
},
)
})
it("returns the create error and does not update the APIService", func() {
startInformersAndController()
err := controller.TestSync(t, subject, *syncContext)
r.EqualError(err, "could not create secret: create failed")
r.Empty(aggregatorAPIClient.Actions())
})
})
})
when("there is an api-serving-cert Secret already in the installation namespace", func() {
it.Before(func() {
apiServingCertSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "api-serving-cert",
Namespace: installedInNamespace,
},
}
err := kubeInformerClient.Tracker().Add(apiServingCertSecret)
r.NoError(err)
})
it("does not need to make any API calls with its API clients", func() {
startInformersAndController()
err := controller.TestSync(t, subject, *syncContext)
r.NoError(err)
r.Empty(kubeAPIClient.Actions())
r.Empty(aggregatorAPIClient.Actions())
})
})
}, spec.Parallel(), spec.Report(report.Terminal{}))
}