864db74306
Signed-off-by: Matt Moyer <moyerm@vmware.com>
353 lines
12 KiB
Go
353 lines
12 KiB
Go
/*
|
|
Copyright 2020 VMware, Inc.
|
|
SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
package apicerts
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"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
|
|
roots := x509.NewCertPool()
|
|
ok := roots.AppendCertsFromPEM([]byte(actualCACert))
|
|
r.True(ok)
|
|
block, _ := pem.Decode([]byte(actualCertChain))
|
|
r.NotNil(block)
|
|
parsedCert, err := x509.ParseCertificate(block.Bytes)
|
|
r.NoError(err)
|
|
serviceEndpoint := "placeholder-name-api." + installedInNamespace + ".svc"
|
|
opts := x509.VerifyOptions{
|
|
DNSName: serviceEndpoint,
|
|
Roots: roots,
|
|
}
|
|
_, err = parsedCert.Verify(opts)
|
|
r.NoError(err)
|
|
r.Contains(parsedCert.DNSNames, serviceEndpoint, "expected an explicit DNS SAN, not just Common Name")
|
|
|
|
// Check the created cert's validity bounds
|
|
r.WithinDuration(time.Now(), parsedCert.NotBefore, time.Minute*2)
|
|
r.WithinDuration(time.Now().Add(24*365*time.Hour), parsedCert.NotAfter, time.Minute*2)
|
|
|
|
// Check that the private key and cert chain match
|
|
_, err = tls.X509KeyPair([]byte(actualCertChain), []byte(actualPrivateKey))
|
|
r.NoError(err)
|
|
|
|
// 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{}))
|
|
}
|