Add tests for the new cert controllers and some other small refactorings
- Add a unit test for each cert controller - Make DynamicTLSServingCertProvider an interface and use a mutex internally - Create a shared ToPEM function instead of having two very similar functions - Move the ObservableWithInformerOption test helper to testutils - Rename some variables and imports
This commit is contained in:
parent
86c3f89b2e
commit
cc9ae23a0c
@ -17,7 +17,7 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
kubetesting "k8s.io/client-go/testing"
|
kubetesting "k8s.io/client-go/testing"
|
||||||
apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
|
apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
|
||||||
aggregationv1fake "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/fake"
|
aggregatorv1fake "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/fake"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestUpdateAPIService(t *testing.T) {
|
func TestUpdateAPIService(t *testing.T) {
|
||||||
@ -25,14 +25,14 @@ func TestUpdateAPIService(t *testing.T) {
|
|||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
mocks func(*aggregationv1fake.Clientset)
|
mocks func(*aggregatorv1fake.Clientset)
|
||||||
caInput []byte
|
caInput []byte
|
||||||
wantObjects []apiregistrationv1.APIService
|
wantObjects []apiregistrationv1.APIService
|
||||||
wantErr string
|
wantErr string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "happy path update when the pre-existing APIService did not already have a CA bundle",
|
name: "happy path update when the pre-existing APIService did not already have a CA bundle",
|
||||||
mocks: func(c *aggregationv1fake.Clientset) {
|
mocks: func(c *aggregatorv1fake.Clientset) {
|
||||||
_ = c.Tracker().Add(&apiregistrationv1.APIService{
|
_ = c.Tracker().Add(&apiregistrationv1.APIService{
|
||||||
ObjectMeta: metav1.ObjectMeta{Name: apiServiceName},
|
ObjectMeta: metav1.ObjectMeta{Name: apiServiceName},
|
||||||
Spec: apiregistrationv1.APIServiceSpec{
|
Spec: apiregistrationv1.APIServiceSpec{
|
||||||
@ -52,7 +52,7 @@ func TestUpdateAPIService(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "happy path update when the pre-existing APIService already had a CA bundle",
|
name: "happy path update when the pre-existing APIService already had a CA bundle",
|
||||||
mocks: func(c *aggregationv1fake.Clientset) {
|
mocks: func(c *aggregatorv1fake.Clientset) {
|
||||||
_ = c.Tracker().Add(&apiregistrationv1.APIService{
|
_ = c.Tracker().Add(&apiregistrationv1.APIService{
|
||||||
ObjectMeta: metav1.ObjectMeta{Name: apiServiceName},
|
ObjectMeta: metav1.ObjectMeta{Name: apiServiceName},
|
||||||
Spec: apiregistrationv1.APIServiceSpec{
|
Spec: apiregistrationv1.APIServiceSpec{
|
||||||
@ -72,7 +72,7 @@ func TestUpdateAPIService(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "error on update",
|
name: "error on update",
|
||||||
mocks: func(c *aggregationv1fake.Clientset) {
|
mocks: func(c *aggregatorv1fake.Clientset) {
|
||||||
_ = c.Tracker().Add(&apiregistrationv1.APIService{
|
_ = c.Tracker().Add(&apiregistrationv1.APIService{
|
||||||
ObjectMeta: metav1.ObjectMeta{Name: apiServiceName},
|
ObjectMeta: metav1.ObjectMeta{Name: apiServiceName},
|
||||||
Spec: apiregistrationv1.APIServiceSpec{},
|
Spec: apiregistrationv1.APIServiceSpec{},
|
||||||
@ -85,7 +85,7 @@ func TestUpdateAPIService(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "error on get",
|
name: "error on get",
|
||||||
mocks: func(c *aggregationv1fake.Clientset) {
|
mocks: func(c *aggregatorv1fake.Clientset) {
|
||||||
_ = c.Tracker().Add(&apiregistrationv1.APIService{
|
_ = c.Tracker().Add(&apiregistrationv1.APIService{
|
||||||
ObjectMeta: metav1.ObjectMeta{Name: apiServiceName},
|
ObjectMeta: metav1.ObjectMeta{Name: apiServiceName},
|
||||||
Spec: apiregistrationv1.APIServiceSpec{},
|
Spec: apiregistrationv1.APIServiceSpec{},
|
||||||
@ -99,7 +99,7 @@ func TestUpdateAPIService(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "conflict error on update, followed by successful retry",
|
name: "conflict error on update, followed by successful retry",
|
||||||
mocks: func(c *aggregationv1fake.Clientset) {
|
mocks: func(c *aggregatorv1fake.Clientset) {
|
||||||
_ = c.Tracker().Add(&apiregistrationv1.APIService{
|
_ = c.Tracker().Add(&apiregistrationv1.APIService{
|
||||||
ObjectMeta: metav1.ObjectMeta{Name: apiServiceName},
|
ObjectMeta: metav1.ObjectMeta{Name: apiServiceName},
|
||||||
Spec: apiregistrationv1.APIServiceSpec{
|
Spec: apiregistrationv1.APIServiceSpec{
|
||||||
@ -148,7 +148,7 @@ func TestUpdateAPIService(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
client := aggregationv1fake.NewSimpleClientset()
|
client := aggregatorv1fake.NewSimpleClientset()
|
||||||
if tt.mocks != nil {
|
if tt.mocks != nil {
|
||||||
tt.mocks(client)
|
tt.mocks(client)
|
||||||
}
|
}
|
||||||
|
@ -195,6 +195,16 @@ func toPEM(cert *tls.Certificate, err error) ([]byte, []byte, error) {
|
|||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
certPEM, keyPEM, err := ToPEM(cert)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return certPEM, keyPEM, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode a tls.Certificate into a private key PEM and a cert chain PEM.
|
||||||
|
func ToPEM(cert *tls.Certificate) ([]byte, []byte, error) {
|
||||||
// Encode the certificate(s) to PEM.
|
// Encode the certificate(s) to PEM.
|
||||||
certPEMBlocks := make([][]byte, 0, len(cert.Certificate))
|
certPEMBlocks := make([][]byte, 0, len(cert.Certificate))
|
||||||
for _, c := range cert.Certificate {
|
for _, c := range cert.Certificate {
|
||||||
|
@ -6,10 +6,7 @@ SPDX-License-Identifier: Apache-2.0
|
|||||||
package apicerts
|
package apicerts
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"crypto/x509/pkix"
|
"crypto/x509/pkix"
|
||||||
"encoding/pem"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -25,7 +22,6 @@ import (
|
|||||||
"github.com/suzerain-io/placeholder-name/internal/autoregistration"
|
"github.com/suzerain-io/placeholder-name/internal/autoregistration"
|
||||||
"github.com/suzerain-io/placeholder-name/internal/certauthority"
|
"github.com/suzerain-io/placeholder-name/internal/certauthority"
|
||||||
placeholdernamecontroller "github.com/suzerain-io/placeholder-name/internal/controller"
|
placeholdernamecontroller "github.com/suzerain-io/placeholder-name/internal/controller"
|
||||||
placeholderv1alpha1 "github.com/suzerain-io/placeholder-name/kubernetes/1.19/api/apis/placeholder/v1alpha1"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -38,28 +34,25 @@ const (
|
|||||||
|
|
||||||
type certsManagerController struct {
|
type certsManagerController struct {
|
||||||
namespace string
|
namespace string
|
||||||
apiServiceName string
|
|
||||||
k8sClient kubernetes.Interface
|
k8sClient kubernetes.Interface
|
||||||
aggregatorClient *aggregatorclient.Clientset
|
aggregatorClient aggregatorclient.Interface
|
||||||
secretInformer corev1informers.SecretInformer
|
secretInformer corev1informers.SecretInformer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCertsManagerController(
|
func NewCertsManagerController(
|
||||||
namespace string,
|
namespace string,
|
||||||
k8sClient kubernetes.Interface,
|
k8sClient kubernetes.Interface,
|
||||||
aggregationClient *aggregatorclient.Clientset,
|
aggregatorClient aggregatorclient.Interface,
|
||||||
secretInformer corev1informers.SecretInformer,
|
secretInformer corev1informers.SecretInformer,
|
||||||
withInformer placeholdernamecontroller.WithInformerOptionFunc,
|
withInformer placeholdernamecontroller.WithInformerOptionFunc,
|
||||||
) controller.Controller {
|
) controller.Controller {
|
||||||
apiServiceName := placeholderv1alpha1.SchemeGroupVersion.Version + "." + placeholderv1alpha1.GroupName
|
|
||||||
return controller.New(
|
return controller.New(
|
||||||
controller.Config{
|
controller.Config{
|
||||||
Name: "certs-manager-controller",
|
Name: "certs-manager-controller",
|
||||||
Syncer: &certsManagerController{
|
Syncer: &certsManagerController{
|
||||||
apiServiceName: apiServiceName,
|
|
||||||
namespace: namespace,
|
namespace: namespace,
|
||||||
k8sClient: k8sClient,
|
k8sClient: k8sClient,
|
||||||
aggregatorClient: aggregationClient,
|
aggregatorClient: aggregatorClient,
|
||||||
secretInformer: secretInformer,
|
secretInformer: secretInformer,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -108,7 +101,7 @@ func (c *certsManagerController) Sync(ctx controller.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Write the CA's public key bundle and the serving certs to a secret.
|
// Write the CA's public key bundle and the serving certs to a secret.
|
||||||
tlsPrivateKeyPEM, tlsCertChainPEM, err := pemEncode(aggregatedAPIServerTLSCert)
|
tlsPrivateKeyPEM, tlsCertChainPEM, err := certauthority.ToPEM(aggregatedAPIServerTLSCert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not PEM encode serving certificate: %w", err)
|
return fmt.Errorf("could not PEM encode serving certificate: %w", err)
|
||||||
}
|
}
|
||||||
@ -137,28 +130,3 @@ func (c *certsManagerController) Sync(ctx controller.Context) error {
|
|||||||
klog.Info("certsManagerController Sync successfully created secret and updated API service")
|
klog.Info("certsManagerController Sync successfully created secret and updated API service")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encode a tls.Certificate into a private key PEM and a cert chain PEM.
|
|
||||||
func pemEncode(cert *tls.Certificate) ([]byte, []byte, error) {
|
|
||||||
privateKeyDER, err := x509.MarshalPKCS8PrivateKey(cert.PrivateKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("error marshalling private key: %w", err)
|
|
||||||
}
|
|
||||||
privateKeyPEM := pem.EncodeToMemory(&pem.Block{
|
|
||||||
Type: "PRIVATE KEY",
|
|
||||||
Headers: nil,
|
|
||||||
Bytes: privateKeyDER,
|
|
||||||
})
|
|
||||||
|
|
||||||
certChainPEM := make([]byte, 0)
|
|
||||||
for _, certFromChain := range cert.Certificate {
|
|
||||||
certPEMBytes := pem.EncodeToMemory(&pem.Block{
|
|
||||||
Type: "CERTIFICATE",
|
|
||||||
Headers: nil,
|
|
||||||
Bytes: certFromChain,
|
|
||||||
})
|
|
||||||
certChainPEM = append(certChainPEM, certPEMBytes...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return privateKeyPEM, certChainPEM, nil
|
|
||||||
}
|
|
||||||
|
333
internal/controller/apicerts/certs_manager_test.go
Normal file
333
internal/controller/apicerts/certs_manager_test.go
Normal file
@ -0,0 +1,333 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 VMware, Inc.
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package apicerts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"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 TestManagerControllerInformerFilters(t *testing.T) {
|
||||||
|
spec.Run(t, "informer filters", func(t *testing.T, when spec.G, it spec.S) {
|
||||||
|
const installedInNamespace = "some-namespace"
|
||||||
|
|
||||||
|
var r *require.Assertions
|
||||||
|
var observableWithInformerOption *testutil.ObservableWithInformerOption
|
||||||
|
var secretsInformerFilter controller.Filter
|
||||||
|
|
||||||
|
it.Before(func() {
|
||||||
|
r = require.New(t)
|
||||||
|
observableWithInformerOption = testutil.NewObservableWithInformerOption()
|
||||||
|
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
|
||||||
|
)
|
||||||
|
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))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, 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,
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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(actualPrivateKey))
|
||||||
|
r.NotNil(block)
|
||||||
|
parsedCert, err := x509.ParseCertificate(block.Bytes)
|
||||||
|
r.NoError(err)
|
||||||
|
opts := x509.VerifyOptions{
|
||||||
|
DNSName: "placeholder-name-api." + installedInNamespace + ".svc",
|
||||||
|
Roots: roots,
|
||||||
|
}
|
||||||
|
_, err = parsedCert.Verify(opts)
|
||||||
|
r.NoError(err)
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
// TODO How can we validate the tlsCertificateChain?
|
||||||
|
|
||||||
|
// 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", func() {
|
||||||
|
startInformersAndController()
|
||||||
|
err := controller.TestSync(t, subject, *syncContext)
|
||||||
|
r.EqualError(err, "could not create secret: create failed")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
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{}))
|
||||||
|
}
|
@ -19,13 +19,13 @@ import (
|
|||||||
|
|
||||||
type certsObserverController struct {
|
type certsObserverController struct {
|
||||||
namespace string
|
namespace string
|
||||||
dynamicCertProvider *provider.DynamicTLSServingCertProvider
|
dynamicCertProvider provider.DynamicTLSServingCertProvider
|
||||||
secretInformer corev1informers.SecretInformer
|
secretInformer corev1informers.SecretInformer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCertsObserverController(
|
func NewCertsObserverController(
|
||||||
namespace string,
|
namespace string,
|
||||||
dynamicCertProvider *provider.DynamicTLSServingCertProvider,
|
dynamicCertProvider provider.DynamicTLSServingCertProvider,
|
||||||
secretInformer corev1informers.SecretInformer,
|
secretInformer corev1informers.SecretInformer,
|
||||||
withInformer placeholdernamecontroller.WithInformerOptionFunc,
|
withInformer placeholdernamecontroller.WithInformerOptionFunc,
|
||||||
) controller.Controller {
|
) controller.Controller {
|
||||||
@ -56,14 +56,12 @@ func (c *certsObserverController) Sync(_ controller.Context) error {
|
|||||||
if notFound {
|
if notFound {
|
||||||
klog.Info("certsObserverController Sync() found that the secret does not exist yet or was deleted")
|
klog.Info("certsObserverController Sync() found that the secret does not exist yet or was deleted")
|
||||||
// The secret does not exist yet or was deleted.
|
// The secret does not exist yet or was deleted.
|
||||||
c.dynamicCertProvider.CertPEM = nil
|
c.dynamicCertProvider.Set(nil, nil)
|
||||||
c.dynamicCertProvider.KeyPEM = nil
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mutate the in-memory cert provider to update with the latest cert values.
|
// Mutate the in-memory cert provider to update with the latest cert values.
|
||||||
c.dynamicCertProvider.CertPEM = certSecret.Data[tlsCertificateChainSecretKey]
|
c.dynamicCertProvider.Set(certSecret.Data[tlsCertificateChainSecretKey], certSecret.Data[tlsPrivateKeySecretKey])
|
||||||
c.dynamicCertProvider.KeyPEM = certSecret.Data[tlsPrivateKeySecretKey]
|
|
||||||
klog.Info("certsObserverController Sync updated certs in the dynamic cert provider")
|
klog.Info("certsObserverController Sync updated certs in the dynamic cert provider")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
232
internal/controller/apicerts/certs_observer_test.go
Normal file
232
internal/controller/apicerts/certs_observer_test.go
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 VMware, Inc.
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package apicerts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"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"
|
||||||
|
kubeinformers "k8s.io/client-go/informers"
|
||||||
|
kubernetesfake "k8s.io/client-go/kubernetes/fake"
|
||||||
|
|
||||||
|
"github.com/suzerain-io/controller-go"
|
||||||
|
"github.com/suzerain-io/placeholder-name/internal/provider"
|
||||||
|
"github.com/suzerain-io/placeholder-name/internal/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestObserverControllerInformerFilters(t *testing.T) {
|
||||||
|
spec.Run(t, "informer filters", func(t *testing.T, when spec.G, it spec.S) {
|
||||||
|
const installedInNamespace = "some-namespace"
|
||||||
|
|
||||||
|
var r *require.Assertions
|
||||||
|
var observableWithInformerOption *testutil.ObservableWithInformerOption
|
||||||
|
var secretsInformerFilter controller.Filter
|
||||||
|
|
||||||
|
it.Before(func() {
|
||||||
|
r = require.New(t)
|
||||||
|
observableWithInformerOption = testutil.NewObservableWithInformerOption()
|
||||||
|
secretsInformer := kubeinformers.NewSharedInformerFactory(nil, 0).Core().V1().Secrets()
|
||||||
|
_ = NewCertsObserverController(
|
||||||
|
installedInNamespace,
|
||||||
|
nil,
|
||||||
|
secretsInformer,
|
||||||
|
observableWithInformerOption.WithInformer, // make it possible to observe the behavior of the Filters
|
||||||
|
)
|
||||||
|
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))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, spec.Parallel(), spec.Report(report.Terminal{}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestObserverControllerSync(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 kubeInformerClient *kubernetesfake.Clientset
|
||||||
|
var kubeInformers kubeinformers.SharedInformerFactory
|
||||||
|
var timeoutContext context.Context
|
||||||
|
var timeoutContextCancel context.CancelFunc
|
||||||
|
var syncContext *controller.Context
|
||||||
|
var dynamicCertProvider provider.DynamicTLSServingCertProvider
|
||||||
|
|
||||||
|
// 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 = NewCertsObserverController(
|
||||||
|
installedInNamespace,
|
||||||
|
dynamicCertProvider,
|
||||||
|
kubeInformers.Core().V1().Secrets(),
|
||||||
|
controller.WithInformer,
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
dynamicCertProvider = provider.NewDynamicTLSServingCertProvider()
|
||||||
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
dynamicCertProvider.Set([]byte("some cert"), []byte("some private key"))
|
||||||
|
})
|
||||||
|
|
||||||
|
it("sets the dynamicCertProvider's cert and key to nil", func() {
|
||||||
|
startInformersAndController()
|
||||||
|
err := controller.TestSync(t, subject, *syncContext)
|
||||||
|
r.NoError(err)
|
||||||
|
|
||||||
|
actualCertChain, actualKey := dynamicCertProvider.CurrentCertKeyContent()
|
||||||
|
r.Nil(actualCertChain)
|
||||||
|
r.Nil(actualKey)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
when("there is an api-serving-cert Secret with the expected keys already in the installation namespace", func() {
|
||||||
|
it.Before(func() {
|
||||||
|
apiServingCertSecret := &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "api-serving-cert",
|
||||||
|
Namespace: installedInNamespace,
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"caCertificate": []byte("fake cert"),
|
||||||
|
"tlsPrivateKey": []byte("fake private key"),
|
||||||
|
"tlsCertificateChain": []byte("fake cert chain"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err := kubeInformerClient.Tracker().Add(apiServingCertSecret)
|
||||||
|
r.NoError(err)
|
||||||
|
|
||||||
|
dynamicCertProvider.Set(nil, nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("updates the dynamicCertProvider's cert and key", func() {
|
||||||
|
startInformersAndController()
|
||||||
|
err := controller.TestSync(t, subject, *syncContext)
|
||||||
|
r.NoError(err)
|
||||||
|
|
||||||
|
actualCertChain, actualKey := dynamicCertProvider.CurrentCertKeyContent()
|
||||||
|
r.Equal("fake cert chain", string(actualCertChain))
|
||||||
|
r.Equal("fake private key", string(actualKey))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
when("the api-serving-cert Secret exists but is missing the expected keys", func() {
|
||||||
|
it.Before(func() {
|
||||||
|
apiServingCertSecret := &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "api-serving-cert",
|
||||||
|
Namespace: installedInNamespace,
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{},
|
||||||
|
}
|
||||||
|
err := kubeInformerClient.Tracker().Add(apiServingCertSecret)
|
||||||
|
r.NoError(err)
|
||||||
|
|
||||||
|
dynamicCertProvider.Set(nil, nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("set the missing values in the dynamicCertProvider as nil", func() {
|
||||||
|
startInformersAndController()
|
||||||
|
err := controller.TestSync(t, subject, *syncContext)
|
||||||
|
r.NoError(err)
|
||||||
|
|
||||||
|
actualCertChain, actualKey := dynamicCertProvider.CurrentCertKeyContent()
|
||||||
|
r.Nil(actualCertChain)
|
||||||
|
r.Nil(actualKey)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, spec.Parallel(), spec.Report(report.Terminal{}))
|
||||||
|
}
|
@ -24,41 +24,24 @@ import (
|
|||||||
coretesting "k8s.io/client-go/testing"
|
coretesting "k8s.io/client-go/testing"
|
||||||
|
|
||||||
"github.com/suzerain-io/controller-go"
|
"github.com/suzerain-io/controller-go"
|
||||||
|
"github.com/suzerain-io/placeholder-name/internal/testutil"
|
||||||
crdsplaceholderv1alpha1 "github.com/suzerain-io/placeholder-name/kubernetes/1.19/api/apis/crdsplaceholder/v1alpha1"
|
crdsplaceholderv1alpha1 "github.com/suzerain-io/placeholder-name/kubernetes/1.19/api/apis/crdsplaceholder/v1alpha1"
|
||||||
placeholderfake "github.com/suzerain-io/placeholder-name/kubernetes/1.19/client-go/clientset/versioned/fake"
|
placeholderfake "github.com/suzerain-io/placeholder-name/kubernetes/1.19/client-go/clientset/versioned/fake"
|
||||||
placeholderinformers "github.com/suzerain-io/placeholder-name/kubernetes/1.19/client-go/informers/externalversions"
|
placeholderinformers "github.com/suzerain-io/placeholder-name/kubernetes/1.19/client-go/informers/externalversions"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ObservableWithInformerOption struct {
|
|
||||||
InformerToFilterMap map[controller.InformerGetter]controller.Filter
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewObservableWithInformerOption() *ObservableWithInformerOption {
|
|
||||||
return &ObservableWithInformerOption{
|
|
||||||
InformerToFilterMap: make(map[controller.InformerGetter]controller.Filter),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (owi *ObservableWithInformerOption) WithInformer(
|
|
||||||
getter controller.InformerGetter,
|
|
||||||
filter controller.Filter,
|
|
||||||
opt controller.InformerOption) controller.Option {
|
|
||||||
owi.InformerToFilterMap[getter] = filter
|
|
||||||
return controller.WithInformer(getter, filter, opt)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInformerFilters(t *testing.T) {
|
func TestInformerFilters(t *testing.T) {
|
||||||
spec.Run(t, "informer filters", func(t *testing.T, when spec.G, it spec.S) {
|
spec.Run(t, "informer filters", func(t *testing.T, when spec.G, it spec.S) {
|
||||||
const installedInNamespace = "some-namespace"
|
const installedInNamespace = "some-namespace"
|
||||||
|
|
||||||
var r *require.Assertions
|
var r *require.Assertions
|
||||||
var observableWithInformerOption *ObservableWithInformerOption
|
var observableWithInformerOption *testutil.ObservableWithInformerOption
|
||||||
var configMapInformerFilter controller.Filter
|
var configMapInformerFilter controller.Filter
|
||||||
var loginDiscoveryConfigInformerFilter controller.Filter
|
var loginDiscoveryConfigInformerFilter controller.Filter
|
||||||
|
|
||||||
it.Before(func() {
|
it.Before(func() {
|
||||||
r = require.New(t)
|
r = require.New(t)
|
||||||
observableWithInformerOption = NewObservableWithInformerOption()
|
observableWithInformerOption = testutil.NewObservableWithInformerOption()
|
||||||
configMapInformer := kubeinformers.NewSharedInformerFactory(nil, 0).Core().V1().ConfigMaps()
|
configMapInformer := kubeinformers.NewSharedInformerFactory(nil, 0).Core().V1().ConfigMaps()
|
||||||
loginDiscoveryConfigInformer := placeholderinformers.NewSharedInformerFactory(nil, 0).Crds().V1alpha1().LoginDiscoveryConfigs()
|
loginDiscoveryConfigInformer := placeholderinformers.NewSharedInformerFactory(nil, 0).Crds().V1alpha1().LoginDiscoveryConfigs()
|
||||||
_ = NewPublisherController(
|
_ = NewPublisherController(
|
||||||
@ -69,8 +52,8 @@ func TestInformerFilters(t *testing.T) {
|
|||||||
loginDiscoveryConfigInformer,
|
loginDiscoveryConfigInformer,
|
||||||
observableWithInformerOption.WithInformer, // make it possible to observe the behavior of the Filters
|
observableWithInformerOption.WithInformer, // make it possible to observe the behavior of the Filters
|
||||||
)
|
)
|
||||||
configMapInformerFilter = observableWithInformerOption.InformerToFilterMap[configMapInformer]
|
configMapInformerFilter = observableWithInformerOption.GetFilterForInformer(configMapInformer)
|
||||||
loginDiscoveryConfigInformerFilter = observableWithInformerOption.InformerToFilterMap[loginDiscoveryConfigInformer]
|
loginDiscoveryConfigInformerFilter = observableWithInformerOption.GetFilterForInformer(loginDiscoveryConfigInformer)
|
||||||
})
|
})
|
||||||
|
|
||||||
when("watching ConfigMap objects", func() {
|
when("watching ConfigMap objects", func() {
|
||||||
|
@ -33,7 +33,7 @@ const (
|
|||||||
func PrepareControllers(
|
func PrepareControllers(
|
||||||
serverInstallationNamespace string,
|
serverInstallationNamespace string,
|
||||||
discoveryURLOverride *string,
|
discoveryURLOverride *string,
|
||||||
dynamicCertProvider *provider.DynamicTLSServingCertProvider,
|
dynamicCertProvider provider.DynamicTLSServingCertProvider,
|
||||||
) (func(ctx context.Context), error) {
|
) (func(ctx context.Context), error) {
|
||||||
// Create k8s clients.
|
// Create k8s clients.
|
||||||
k8sClient, aggregatorClient, placeholderClient, err := createClients()
|
k8sClient, aggregatorClient, placeholderClient, err := createClients()
|
||||||
|
@ -5,15 +5,40 @@ SPDX-License-Identifier: Apache-2.0
|
|||||||
|
|
||||||
package provider
|
package provider
|
||||||
|
|
||||||
type DynamicTLSServingCertProvider struct {
|
import (
|
||||||
CertPEM []byte
|
"sync"
|
||||||
KeyPEM []byte
|
|
||||||
|
"k8s.io/apiserver/pkg/server/dynamiccertificates"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DynamicTLSServingCertProvider interface {
|
||||||
|
dynamiccertificates.CertKeyContentProvider
|
||||||
|
Set(certPEM, keyPEM []byte)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*DynamicTLSServingCertProvider) Name() string {
|
type dynamicTLSServingCertProvider struct {
|
||||||
|
certPEM []byte
|
||||||
|
keyPEM []byte
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDynamicTLSServingCertProvider() DynamicTLSServingCertProvider {
|
||||||
|
return &dynamicTLSServingCertProvider{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *dynamicTLSServingCertProvider) Set(certPEM, keyPEM []byte) {
|
||||||
|
p.mutex.Lock() // acquire a write lock
|
||||||
|
defer p.mutex.Unlock()
|
||||||
|
p.certPEM = certPEM
|
||||||
|
p.keyPEM = keyPEM
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *dynamicTLSServingCertProvider) Name() string {
|
||||||
return "DynamicTLSServingCertProvider"
|
return "DynamicTLSServingCertProvider"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *DynamicTLSServingCertProvider) CurrentCertKeyContent() (cert []byte, key []byte) {
|
func (p *dynamicTLSServingCertProvider) CurrentCertKeyContent() (cert []byte, key []byte) {
|
||||||
return p.CertPEM, p.KeyPEM
|
p.mutex.RLock() // acquire a read lock
|
||||||
|
defer p.mutex.RUnlock()
|
||||||
|
return p.certPEM, p.keyPEM
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ import (
|
|||||||
|
|
||||||
// App is an object that represents the placeholder-name-server application.
|
// App is an object that represents the placeholder-name-server application.
|
||||||
type App struct {
|
type App struct {
|
||||||
serverCommand *cobra.Command
|
cmd *cobra.Command
|
||||||
|
|
||||||
// CLI flags
|
// CLI flags
|
||||||
configPath string
|
configPath string
|
||||||
@ -48,27 +48,27 @@ func New(ctx context.Context, args []string, stdout, stderr io.Writer) *App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run the server.
|
// Run the server.
|
||||||
func (app *App) Run() error {
|
func (a *App) Run() error {
|
||||||
return app.serverCommand.Execute()
|
return a.cmd.Execute()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the server command and save it into the App.
|
// Create the server command and save it into the App.
|
||||||
func (app *App) addServerCommand(ctx context.Context, args []string, stdout, stderr io.Writer) {
|
func (a *App) addServerCommand(ctx context.Context, args []string, stdout, stderr io.Writer) {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: `placeholder-name-server`,
|
Use: `placeholder-name-server`,
|
||||||
Long: "placeholder-name-server provides a generic API for mapping an external\n" +
|
Long: "placeholder-name-server provides a generic API for mapping an external\n" +
|
||||||
"credential from somewhere to an internal credential to be used for\n" +
|
"credential from somewhere to an internal credential to be used for\n" +
|
||||||
"authenticating to the Kubernetes API.",
|
"authenticating to the Kubernetes API.",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error { return app.runServer(ctx) },
|
RunE: func(cmd *cobra.Command, args []string) error { return a.runServer(ctx) },
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.SetArgs(args)
|
cmd.SetArgs(args)
|
||||||
cmd.SetOut(stdout)
|
cmd.SetOut(stdout)
|
||||||
cmd.SetErr(stderr)
|
cmd.SetErr(stderr)
|
||||||
addCommandlineFlagsToCommand(cmd, app)
|
addCommandlineFlagsToCommand(cmd, a)
|
||||||
|
|
||||||
app.serverCommand = cmd
|
a.cmd = cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define the app's commandline flags.
|
// Define the app's commandline flags.
|
||||||
@ -104,15 +104,15 @@ func addCommandlineFlagsToCommand(cmd *cobra.Command, app *App) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Boot the aggregated API server, which will in turn boot the controllers.
|
// Boot the aggregated API server, which will in turn boot the controllers.
|
||||||
func (app *App) runServer(ctx context.Context) error {
|
func (a *App) runServer(ctx context.Context) error {
|
||||||
// Read the server config file.
|
// Read the server config file.
|
||||||
cfg, err := config.FromPath(app.configPath)
|
cfg, err := config.FromPath(a.configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not load config: %w", err)
|
return fmt.Errorf("could not load config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the Kubernetes cluster signing CA.
|
// Load the Kubernetes cluster signing CA.
|
||||||
k8sClusterCA, err := certauthority.Load(app.clusterSigningCertFilePath, app.clusterSigningKeyFilePath)
|
k8sClusterCA, err := certauthority.Load(a.clusterSigningCertFilePath, a.clusterSigningKeyFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not load cluster signing CA: %w", err)
|
return fmt.Errorf("could not load cluster signing CA: %w", err)
|
||||||
}
|
}
|
||||||
@ -124,7 +124,7 @@ func (app *App) runServer(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Discover in which namespace we are installed.
|
// Discover in which namespace we are installed.
|
||||||
podInfo, err := downward.Load(app.downwardAPIPath)
|
podInfo, err := downward.Load(a.downwardAPIPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("could not read pod metadata: %w", err)
|
return fmt.Errorf("could not read pod metadata: %w", err)
|
||||||
}
|
}
|
||||||
@ -135,7 +135,7 @@ func (app *App) runServer(ctx context.Context) error {
|
|||||||
// is stored in a k8s Secret. Therefore it also effectively acting as
|
// is stored in a k8s Secret. Therefore it also effectively acting as
|
||||||
// an in-memory cache of what is stored in the k8s Secret, helping to
|
// an in-memory cache of what is stored in the k8s Secret, helping to
|
||||||
// keep incoming requests fast.
|
// keep incoming requests fast.
|
||||||
dynamicCertProvider := &provider.DynamicTLSServingCertProvider{}
|
dynamicCertProvider := provider.NewDynamicTLSServingCertProvider()
|
||||||
|
|
||||||
// Prepare to start the controllers, but defer actually starting them until the
|
// Prepare to start the controllers, but defer actually starting them until the
|
||||||
// post start hook of the aggregated API server.
|
// post start hook of the aggregated API server.
|
||||||
@ -171,7 +171,7 @@ func (app *App) runServer(ctx context.Context) error {
|
|||||||
|
|
||||||
// Create a configuration for the aggregated API server.
|
// Create a configuration for the aggregated API server.
|
||||||
func getAggregatedAPIServerConfig(
|
func getAggregatedAPIServerConfig(
|
||||||
dynamicCertProvider *provider.DynamicTLSServingCertProvider,
|
dynamicCertProvider provider.DynamicTLSServingCertProvider,
|
||||||
webhookTokenAuthenticator *webhook.WebhookTokenAuthenticator,
|
webhookTokenAuthenticator *webhook.WebhookTokenAuthenticator,
|
||||||
ca *certauthority.CA,
|
ca *certauthority.CA,
|
||||||
startControllersPostStartHook func(context.Context),
|
startControllersPostStartHook func(context.Context),
|
||||||
|
@ -78,7 +78,7 @@ func TestCommand(t *testing.T) {
|
|||||||
stderr := bytes.NewBuffer([]byte{})
|
stderr := bytes.NewBuffer([]byte{})
|
||||||
|
|
||||||
a := New(context.Background(), test.args, stdout, stderr)
|
a := New(context.Background(), test.args, stdout, stderr)
|
||||||
a.serverCommand.RunE = func(cmd *cobra.Command, args []string) error {
|
a.cmd.RunE = func(cmd *cobra.Command, args []string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
err := a.Run()
|
err := a.Run()
|
||||||
|
30
internal/testutil/observable_with_informer_option.go
Normal file
30
internal/testutil/observable_with_informer_option.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 VMware, Inc.
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package testutil
|
||||||
|
|
||||||
|
import "github.com/suzerain-io/controller-go"
|
||||||
|
|
||||||
|
type ObservableWithInformerOption struct {
|
||||||
|
InformerToFilterMap map[controller.InformerGetter]controller.Filter
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewObservableWithInformerOption() *ObservableWithInformerOption {
|
||||||
|
return &ObservableWithInformerOption{
|
||||||
|
InformerToFilterMap: make(map[controller.InformerGetter]controller.Filter),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *ObservableWithInformerOption) WithInformer(
|
||||||
|
getter controller.InformerGetter,
|
||||||
|
filter controller.Filter,
|
||||||
|
opt controller.InformerOption) controller.Option {
|
||||||
|
i.InformerToFilterMap[getter] = filter
|
||||||
|
return controller.WithInformer(getter, filter, opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *ObservableWithInformerOption) GetFilterForInformer(getter controller.InformerGetter) controller.Filter {
|
||||||
|
return i.InformerToFilterMap[getter]
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user