ContainerImage.Pinniped/internal/controller/supervisorconfig/tls_cert_observer_test.go
Ryan Richard c6c2c525a6 Upgrade the linter and fix all new linter warnings
Also fix some tests that were broken by bumping golang and dependencies
in the previous commits.

Note that in addition to changes made to satisfy the linter which do not
impact the behavior of the code, this commit also adds ReadHeaderTimeout
to all usages of http.Server to satisfy the linter (and because it
seemed like a good suggestion).
2022-08-24 14:45:55 -07:00

391 lines
16 KiB
Go

// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package supervisorconfig
import (
"context"
"crypto/tls"
"net/url"
"os"
"testing"
"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"
"go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
pinnipedfake "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake"
pinnipedinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions"
"go.pinniped.dev/internal/controllerlib"
"go.pinniped.dev/internal/testutil"
)
func TestTLSCertObserverControllerInformerFilters(t *testing.T) {
spec.Run(t, "informer filters", func(t *testing.T, when spec.G, it spec.S) {
var (
r *require.Assertions
observableWithInformerOption *testutil.ObservableWithInformerOption
secretsInformerFilter controllerlib.Filter
federationDomainInformerFilter controllerlib.Filter
)
it.Before(func() {
r = require.New(t)
observableWithInformerOption = testutil.NewObservableWithInformerOption()
secretsInformer := kubeinformers.NewSharedInformerFactory(nil, 0).Core().V1().Secrets()
federationDomainInformer := pinnipedinformers.NewSharedInformerFactory(nil, 0).Config().V1alpha1().FederationDomains()
_ = NewTLSCertObserverController(
nil,
"", // don't care about the secret name for this test
secretsInformer,
federationDomainInformer,
observableWithInformerOption.WithInformer, // make it possible to observe the behavior of the Filters
)
secretsInformerFilter = observableWithInformerOption.GetFilterForInformer(secretsInformer)
federationDomainInformerFilter = observableWithInformerOption.GetFilterForInformer(federationDomainInformer)
})
when("watching Secret objects", func() {
var (
subject controllerlib.Filter
secret, otherSecret *corev1.Secret
)
it.Before(func() {
subject = secretsInformerFilter
secret = &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "any-name", Namespace: "any-namespace"}, Type: corev1.SecretTypeTLS}
otherSecret = &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "any-other-name", Namespace: "any-other-namespace"}, Type: "other type"}
})
when("any Secret of type TLS changes", func() {
it("returns true to trigger the sync method", func() {
r.True(subject.Add(secret))
r.True(subject.Update(secret, otherSecret))
r.True(subject.Update(otherSecret, secret))
r.True(subject.Delete(secret))
})
})
when("any Secret that is not of type TLS changes", func() {
it("returns false to avoid triggering the sync method", func() {
r.False(subject.Add(otherSecret))
r.False(subject.Update(otherSecret, otherSecret))
r.False(subject.Delete(otherSecret))
})
})
})
when("watching FederationDomain objects", func() {
var (
subject controllerlib.Filter
provider, otherProvider *v1alpha1.FederationDomain
)
it.Before(func() {
subject = federationDomainInformerFilter
provider = &v1alpha1.FederationDomain{ObjectMeta: metav1.ObjectMeta{Name: "any-name", Namespace: "any-namespace"}}
otherProvider = &v1alpha1.FederationDomain{ObjectMeta: metav1.ObjectMeta{Name: "any-other-name", Namespace: "any-other-namespace"}}
})
when("any FederationDomain changes", func() {
it("returns true to trigger the sync method", func() {
r.True(subject.Add(provider))
r.True(subject.Update(provider, otherProvider))
r.True(subject.Update(otherProvider, provider))
r.True(subject.Delete(provider))
})
})
})
}, spec.Parallel(), spec.Report(report.Terminal{}))
}
type fakeIssuerTLSCertSetter struct {
setIssuerHostToTLSCertMapWasCalled bool
setDefaultTLSCertWasCalled bool
issuerHostToTLSCertMapReceived map[string]*tls.Certificate
setDefaultTLSCertReceived *tls.Certificate
}
func (f *fakeIssuerTLSCertSetter) SetIssuerHostToTLSCertMap(issuerHostToTLSCertMap map[string]*tls.Certificate) {
f.setIssuerHostToTLSCertMapWasCalled = true
f.issuerHostToTLSCertMapReceived = issuerHostToTLSCertMap
}
func (f *fakeIssuerTLSCertSetter) SetDefaultTLSCert(certificate *tls.Certificate) {
f.setDefaultTLSCertWasCalled = true
f.setDefaultTLSCertReceived = certificate
}
func TestTLSCertObserverControllerSync(t *testing.T) {
spec.Run(t, "Sync", func(t *testing.T, when spec.G, it spec.S) {
const (
installedInNamespace = "some-namespace"
defaultTLSSecretName = "some-default-secret-name"
)
var (
r *require.Assertions
subject controllerlib.Controller
pinnipedInformerClient *pinnipedfake.Clientset
kubeInformerClient *kubernetesfake.Clientset
pinnipedInformers pinnipedinformers.SharedInformerFactory
kubeInformers kubeinformers.SharedInformerFactory
cancelContext context.Context
cancelContextCancelFunc context.CancelFunc
syncContext *controllerlib.Context
issuerTLSCertSetter *fakeIssuerTLSCertSetter
)
// 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 = NewTLSCertObserverController(
issuerTLSCertSetter,
defaultTLSSecretName,
kubeInformers.Core().V1().Secrets(),
pinnipedInformers.Config().V1alpha1().FederationDomains(),
controllerlib.WithInformer,
)
// Set this at the last second to support calling subject.Name().
syncContext = &controllerlib.Context{
Context: cancelContext,
Name: subject.Name(),
Key: controllerlib.Key{
Namespace: installedInNamespace,
Name: "any-name",
},
}
// Must start informers before calling TestRunSynchronously()
kubeInformers.Start(cancelContext.Done())
pinnipedInformers.Start(cancelContext.Done())
controllerlib.TestRunSynchronously(t, subject)
}
var readTestFile = func(path string) []byte {
data, err := os.ReadFile(path)
r.NoError(err)
return data
}
it.Before(func() {
r = require.New(t)
cancelContext, cancelContextCancelFunc = context.WithCancel(context.Background())
kubeInformerClient = kubernetesfake.NewSimpleClientset()
kubeInformers = kubeinformers.NewSharedInformerFactory(kubeInformerClient, 0)
pinnipedInformerClient = pinnipedfake.NewSimpleClientset()
pinnipedInformers = pinnipedinformers.NewSharedInformerFactory(pinnipedInformerClient, 0)
issuerTLSCertSetter = &fakeIssuerTLSCertSetter{}
unrelatedSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "some other unrelated secret",
Namespace: installedInNamespace,
},
}
r.NoError(kubeInformerClient.Tracker().Add(unrelatedSecret))
})
it.After(func() {
cancelContextCancelFunc()
})
when("there are no FederationDomains and no TLS Secrets yet", func() {
it("sets the issuerTLSCertSetter's map to be empty", func() {
startInformersAndController()
err := controllerlib.TestSync(t, subject, *syncContext)
r.NoError(err)
r.True(issuerTLSCertSetter.setIssuerHostToTLSCertMapWasCalled)
r.Empty(issuerTLSCertSetter.issuerHostToTLSCertMapReceived)
r.True(issuerTLSCertSetter.setDefaultTLSCertWasCalled)
r.Nil(issuerTLSCertSetter.setDefaultTLSCertReceived)
})
})
when("there are FederationDomains where some have corresponding TLS Secrets and some don't", func() {
var (
expectedCertificate1, expectedCertificate2 tls.Certificate
)
it.Before(func() {
var err error
federationDomainWithoutSecret1 := &v1alpha1.FederationDomain{
ObjectMeta: metav1.ObjectMeta{
Name: "no-secret-federationdomain1",
Namespace: installedInNamespace,
},
Spec: v1alpha1.FederationDomainSpec{Issuer: "https://no-secret-issuer1.com"}, // no SNICertificateSecretName field
}
federationDomainWithoutSecret2 := &v1alpha1.FederationDomain{
ObjectMeta: metav1.ObjectMeta{
Name: "no-secret-federationdomain2",
Namespace: installedInNamespace,
},
Spec: v1alpha1.FederationDomainSpec{
Issuer: "https://no-secret-issuer2.com",
TLS: &v1alpha1.FederationDomainTLSSpec{SecretName: ""},
},
}
federationDomainWithBadSecret := &v1alpha1.FederationDomain{
ObjectMeta: metav1.ObjectMeta{
Name: "bad-secret-federationdomain",
Namespace: installedInNamespace,
},
Spec: v1alpha1.FederationDomainSpec{
Issuer: "https://bad-secret-issuer.com",
TLS: &v1alpha1.FederationDomainTLSSpec{SecretName: "bad-tls-secret-name"},
},
}
// Also add one with a URL that cannot be parsed to make sure that the controller is not confused by invalid URLs.
invalidIssuerURL := ":/host//path"
_, err = url.Parse(invalidIssuerURL) //nolint:staticcheck // Yes, this URL is intentionally invalid.
r.Error(err)
federationDomainWithBadIssuer := &v1alpha1.FederationDomain{
ObjectMeta: metav1.ObjectMeta{
Name: "bad-issuer-federationdomain",
Namespace: installedInNamespace,
},
Spec: v1alpha1.FederationDomainSpec{Issuer: invalidIssuerURL},
}
federationDomainWithGoodSecret1 := &v1alpha1.FederationDomain{
ObjectMeta: metav1.ObjectMeta{
Name: "good-secret-federationdomain1",
Namespace: installedInNamespace,
},
// Issuer hostname should be treated in a case-insensitive way and SNI ignores port numbers. Test without a port number.
Spec: v1alpha1.FederationDomainSpec{
Issuer: "https://www.iSSuer-wiTh-goOd-secRet1.cOm/path",
TLS: &v1alpha1.FederationDomainTLSSpec{SecretName: "good-tls-secret-name1"},
},
}
federationDomainWithGoodSecret2 := &v1alpha1.FederationDomain{
ObjectMeta: metav1.ObjectMeta{
Name: "good-secret-federationdomain2",
Namespace: installedInNamespace,
},
// Issuer hostname should be treated in a case-insensitive way and SNI ignores port numbers. Test with a port number.
Spec: v1alpha1.FederationDomainSpec{
Issuer: "https://www.issUEr-WIth-gOOd-seCret2.com:1234/path",
TLS: &v1alpha1.FederationDomainTLSSpec{SecretName: "good-tls-secret-name2"},
},
}
federationDomainWithIPv6Issuer := &v1alpha1.FederationDomain{
ObjectMeta: metav1.ObjectMeta{
Name: "ipv6-issuer-federationdomain",
Namespace: installedInNamespace,
},
// Issuer hostname should be treated correctly when it is an IPv6 address. Test with a port number.
Spec: v1alpha1.FederationDomainSpec{
Issuer: "https://[2001:db8::1]:1234/path",
TLS: &v1alpha1.FederationDomainTLSSpec{SecretName: "good-tls-secret-name1"},
},
}
testCrt1 := readTestFile("testdata/test.crt")
r.NotEmpty(testCrt1)
testCrt2 := readTestFile("testdata/test2.crt")
r.NotEmpty(testCrt2)
testKey1 := readTestFile("testdata/test.key")
r.NotEmpty(testKey1)
testKey2 := readTestFile("testdata/test2.key")
r.NotEmpty(testKey2)
expectedCertificate1, err = tls.X509KeyPair(testCrt1, testKey1)
r.NoError(err)
expectedCertificate2, err = tls.X509KeyPair(testCrt2, testKey2)
r.NoError(err)
goodTLSSecret1 := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "good-tls-secret-name1", Namespace: installedInNamespace},
Data: map[string][]byte{"tls.crt": testCrt1, "tls.key": testKey1},
}
goodTLSSecret2 := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "good-tls-secret-name2", Namespace: installedInNamespace},
Data: map[string][]byte{"tls.crt": testCrt2, "tls.key": testKey2},
}
badTLSSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "bad-tls-secret-name", Namespace: installedInNamespace},
Data: map[string][]byte{"junk": nil},
}
r.NoError(pinnipedInformerClient.Tracker().Add(federationDomainWithoutSecret1))
r.NoError(pinnipedInformerClient.Tracker().Add(federationDomainWithoutSecret2))
r.NoError(pinnipedInformerClient.Tracker().Add(federationDomainWithBadSecret))
r.NoError(pinnipedInformerClient.Tracker().Add(federationDomainWithBadIssuer))
r.NoError(pinnipedInformerClient.Tracker().Add(federationDomainWithGoodSecret1))
r.NoError(pinnipedInformerClient.Tracker().Add(federationDomainWithGoodSecret2))
r.NoError(pinnipedInformerClient.Tracker().Add(federationDomainWithIPv6Issuer))
r.NoError(kubeInformerClient.Tracker().Add(goodTLSSecret1))
r.NoError(kubeInformerClient.Tracker().Add(goodTLSSecret2))
r.NoError(kubeInformerClient.Tracker().Add(badTLSSecret))
})
it("updates the issuerTLSCertSetter's map to include only the issuers that had valid certs", func() {
startInformersAndController()
r.NoError(controllerlib.TestSync(t, subject, *syncContext))
r.True(issuerTLSCertSetter.setDefaultTLSCertWasCalled)
r.Nil(issuerTLSCertSetter.setDefaultTLSCertReceived)
r.True(issuerTLSCertSetter.setIssuerHostToTLSCertMapWasCalled)
r.Len(issuerTLSCertSetter.issuerHostToTLSCertMapReceived, 3)
// They keys in the map should be lower case and should not include the port numbers, because
// TLS SNI says that SNI hostnames must be DNS names (not ports) and must be case insensitive.
// See https://tools.ietf.org/html/rfc3546#section-3.1
actualCertificate1 := issuerTLSCertSetter.issuerHostToTLSCertMapReceived["www.issuer-with-good-secret1.com"]
r.NotNil(actualCertificate1)
// The actual cert should match the one from the test fixture that was put into the secret.
r.Equal(expectedCertificate1, *actualCertificate1)
actualCertificate2 := issuerTLSCertSetter.issuerHostToTLSCertMapReceived["www.issuer-with-good-secret2.com"]
r.NotNil(actualCertificate2)
r.Equal(expectedCertificate2, *actualCertificate2)
actualCertificate3 := issuerTLSCertSetter.issuerHostToTLSCertMapReceived["2001:db8::1"]
r.NotNil(actualCertificate3)
r.Equal(expectedCertificate1, *actualCertificate3)
})
when("there is also a default TLS cert secret with the configured default TLS cert secret name", func() {
var (
expectedDefaultCertificate tls.Certificate
)
it.Before(func() {
var err error
testCrt := readTestFile("testdata/test3.crt")
r.NotEmpty(testCrt)
testKey := readTestFile("testdata/test3.key")
r.NotEmpty(testKey)
expectedDefaultCertificate, err = tls.X509KeyPair(testCrt, testKey)
r.NoError(err)
defaultTLSCertSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: defaultTLSSecretName, Namespace: installedInNamespace},
Data: map[string][]byte{"tls.crt": testCrt, "tls.key": testKey},
}
r.NoError(kubeInformerClient.Tracker().Add(defaultTLSCertSecret))
})
it("updates the issuerTLSCertSetter's map as before but also updates the default certificate", func() {
startInformersAndController()
r.NoError(controllerlib.TestSync(t, subject, *syncContext))
r.True(issuerTLSCertSetter.setDefaultTLSCertWasCalled)
actualDefaultCertificate := issuerTLSCertSetter.setDefaultTLSCertReceived
r.NotNil(actualDefaultCertificate)
r.Equal(expectedDefaultCertificate, *actualDefaultCertificate)
r.True(issuerTLSCertSetter.setIssuerHostToTLSCertMapWasCalled)
r.Len(issuerTLSCertSetter.issuerHostToTLSCertMapReceived, 3)
})
})
})
}, spec.Parallel(), spec.Report(report.Terminal{}))
}