From 326b53338fade1bf60a15f7d2c542b338af4502d Mon Sep 17 00:00:00 2001 From: Periyasamy Palanisamy Date: Fri, 8 Nov 2024 10:39:05 +0100 Subject: [PATCH] Add unit tests for cert signer Signed-off-by: Periyasamy Palanisamy --- pkg/controller/signer/signer_test.go | 273 +++++++++++++++++++++++++++ 1 file changed, 273 insertions(+) create mode 100644 pkg/controller/signer/signer_test.go diff --git a/pkg/controller/signer/signer_test.go b/pkg/controller/signer/signer_test.go new file mode 100644 index 0000000000..cf7f7cfcb6 --- /dev/null +++ b/pkg/controller/signer/signer_test.go @@ -0,0 +1,273 @@ +package signer + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "math/big" + "testing" + "time" + + . "github.com/onsi/gomega" + configv1 "github.com/openshift/api/config/v1" + operv1 "github.com/openshift/api/operator/v1" + cnoclient "github.com/openshift/cluster-network-operator/pkg/client" + "github.com/openshift/cluster-network-operator/pkg/client/fake" + "github.com/openshift/cluster-network-operator/pkg/controller/statusmanager" + "github.com/openshift/cluster-network-operator/pkg/names" + certificatev1 "k8s.io/api/certificates/v1" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +var ( + csrName = "ipsec-csr" + nodeName = "testnode" + coName = "testing" +) + +//nolint:errcheck +func init() { + certificatev1.AddToScheme(scheme.Scheme) + corev1.AddToScheme(scheme.Scheme) +} + +func TestSigner_reconciler(t *testing.T) { + g := NewGomegaWithT(t) + client := fake.NewFakeClient() + status := statusmanager.New(client, coName, names.StandAloneClusterName) + signer := ReconcileCSR{client: client, status: status} + + co := &configv1.ClusterOperator{ObjectMeta: metav1.ObjectMeta{Name: coName}} + setCO(t, client, co) + no := &operv1.Network{ObjectMeta: metav1.ObjectMeta{Name: names.OPERATOR_CONFIG}} + setOC(t, client, no) + + csr, err := generateCSR() + g.Expect(err).NotTo(HaveOccurred()) + csrObj := &certificatev1.CertificateSigningRequest{} + csrObj.Name = csrName + csrObj.Spec.Request = []byte(csr) + csrObj.Spec.SignerName = signerName + csrObj.Spec.Usages = []certificatev1.KeyUsage{"ipsec tunnel"} + csrObj.Spec.Username = fmt.Sprintf("system:ovn-node:%s", nodeName) + csrObj.Status.Conditions = append(csrObj.Status.Conditions, certificatev1.CertificateSigningRequestCondition{ + Type: certificatev1.CertificateApproved, + Status: "True", + Reason: "AutoApproved", + Message: "Automatically approved by " + signerName}) + + err = client.Default().CRClient().Create(context.TODO(), csrObj) + g.Expect(err).NotTo(HaveOccurred()) + _, err = client.Default().Kubernetes().CertificatesV1().CertificateSigningRequests().Create(context.TODO(), csrObj, v1.CreateOptions{}) + g.Expect(err).NotTo(HaveOccurred()) + + node := &corev1.Node{} + node.Name = nodeName + _, err = client.Default().Kubernetes().CoreV1().Nodes().Create(context.TODO(), node, v1.CreateOptions{}) + g.Expect(err).NotTo(HaveOccurred()) + + caKey, caCert, err := generateSelfSignedCACertificate() + g.Expect(err).NotTo(HaveOccurred()) + caSecret := &corev1.Secret{} + caSecret.Name = "signer-ca" + caSecret.Namespace = "openshift-ovn-kubernetes" + caSecret.Data = make(map[string][]byte) + caSecret.Data["tls.crt"] = []byte(caCert) + caSecret.Data["tls.key"] = []byte(caKey) + err = client.Default().CRClient().Create(context.TODO(), caSecret) + g.Expect(err).NotTo(HaveOccurred()) + + _, err = signer.Reconcile(context.TODO(), + reconcile.Request{NamespacedName: types.NamespacedName{Name: csrName}}) + g.Expect(err).NotTo(HaveOccurred()) + + err = client.Default().CRClient().Get(context.TODO(), types.NamespacedName{Name: csrName}, csrObj) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(csrObj.Status.Certificate).ShouldNot(BeEmpty()) + + co, _, err = getStatuses(client, "testing") + if err != nil { + t.Fatalf("error getting network.operator: %v", err) + } + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(conditionsInclude(co.Status.Conditions, []configv1.ClusterOperatorStatusCondition{ + { + Type: configv1.OperatorDegraded, + Status: configv1.ConditionFalse, + }, + })).To(BeTrue()) + g.Expect(conditionsInclude(co.Status.Conditions, []configv1.ClusterOperatorStatusCondition{ + { + Type: configv1.OperatorUpgradeable, + Status: configv1.ConditionTrue, + }, + })).To(BeTrue()) +} + +func TestSigner_reconciler_withInvalidUserName(t *testing.T) { + g := NewGomegaWithT(t) + client := fake.NewFakeClient() + status := statusmanager.New(client, coName, names.StandAloneClusterName) + signer := ReconcileCSR{client: client, status: status} + + co := &configv1.ClusterOperator{ObjectMeta: metav1.ObjectMeta{Name: coName}} + setCO(t, client, co) + no := &operv1.Network{ObjectMeta: metav1.ObjectMeta{Name: names.OPERATOR_CONFIG}} + setOC(t, client, no) + + csr, err := generateCSR() + g.Expect(err).NotTo(HaveOccurred()) + csrObj := &certificatev1.CertificateSigningRequest{} + csrObj.Name = csrName + csrObj.Spec.Request = []byte(csr) + csrObj.Spec.SignerName = signerName + csrObj.Spec.Usages = []certificatev1.KeyUsage{"ipsec tunnel"} + csrObj.Spec.Username = fmt.Sprintf("system:ovn-node:%s", "suspicious-node") + + err = client.Default().CRClient().Create(context.TODO(), csrObj) + g.Expect(err).NotTo(HaveOccurred()) + _, err = client.Default().Kubernetes().CertificatesV1().CertificateSigningRequests().Create(context.TODO(), csrObj, v1.CreateOptions{}) + g.Expect(err).NotTo(HaveOccurred()) + + node := &corev1.Node{} + node.Name = nodeName + _, err = client.Default().Kubernetes().CoreV1().Nodes().Create(context.TODO(), node, v1.CreateOptions{}) + g.Expect(err).NotTo(HaveOccurred()) + + _, err = signer.Reconcile(context.TODO(), + reconcile.Request{NamespacedName: types.NamespacedName{Name: csrName}}) + g.Expect(err).NotTo(HaveOccurred()) + + err = client.Default().CRClient().Get(context.TODO(), types.NamespacedName{Name: csrName}, csrObj) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(csrObj.Status.Certificate).Should(BeEmpty()) + csrConditions := csrObj.Status.Conditions + g.Expect(len(csrConditions)).To(Equal(1)) + g.Expect(csrConditions[0].Reason).To(Equal("CSRInvalidUser")) + g.Expect(csrConditions[0].Type).To(Equal(certificatev1.CertificateFailed)) +} + +func generateSelfSignedCACertificate() (string, string, error) { + // Create private key. + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return "", "", fmt.Errorf("failed to generate private key: %v", err) + } + // Create a certificate template for CA certificate. + certTemplate := x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + Organization: []string{"OpenShift"}, + CommonName: signerName, + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(10 * time.Minute), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, + BasicConstraintsValid: true, + IsCA: true, + MaxPathLen: 0, + } + // Self-sign the certificate using the private key. + certDER, err := x509.CreateCertificate(rand.Reader, &certTemplate, &certTemplate, &privateKey.PublicKey, privateKey) + if err != nil { + return "", "", fmt.Errorf("failed to create ca certificate: %v", err) + } + // Encode CA private key in PEM format. + privateKeyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)}) + if privateKeyPEM == nil { + return "", "", fmt.Errorf("failed to encode private key in PEM format") + } + // Encode CA certificate in PEM format. + certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}) + if certPEM == nil { + return "", "", fmt.Errorf("failed to encode certificate in PEM format") + } + return string(privateKeyPEM), string(certPEM), nil +} + +func generateCSR() (string, error) { + // Create private key. + csrKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return "", fmt.Errorf("failed to generate private key: %v", err) + } + // Create CSR with private key. + csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{}, csrKey) + if err != nil { + return "", err + } + // Encode CSR in PEM format. + csrPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrBytes}) + if csrPEM == nil { + return "", fmt.Errorf("failed to encode CSR in PEM format") + } + return string(csrPEM), nil +} + +func setOC(t *testing.T, client cnoclient.Client, oc *operv1.Network) { + t.Helper() + g := NewGomegaWithT(t) + _, err := client.Default().OpenshiftOperatorClient().OperatorV1().Networks().Update(context.TODO(), oc, metav1.UpdateOptions{}) + if apierrors.IsNotFound(err) { + _, err = client.Default().OpenshiftOperatorClient().OperatorV1().Networks().Create(context.TODO(), oc, metav1.CreateOptions{}) + } + g.Expect(err).NotTo(HaveOccurred()) +} + +func setCO(t *testing.T, client cnoclient.Client, co *configv1.ClusterOperator) { + t.Helper() + g := NewGomegaWithT(t) + err := client.Default().CRClient().Update(context.TODO(), co) + if apierrors.IsNotFound(err) { + err = client.Default().CRClient().Create(context.TODO(), co) + } + g.Expect(err).NotTo(HaveOccurred()) +} + +func getStatuses(client cnoclient.Client, name string) (*configv1.ClusterOperator, *operv1.Network, error) { + co := &configv1.ClusterOperator{ObjectMeta: metav1.ObjectMeta{Name: name}} + err := client.ClientFor("").CRClient().Get(context.TODO(), types.NamespacedName{Name: name}, co) + if err != nil { + return nil, nil, err + } + oc, err := client.Default().OpenshiftOperatorClient().OperatorV1().Networks().Get(context.TODO(), names.OPERATOR_CONFIG, metav1.GetOptions{}) + return co, oc, err +} + +// Tests that the parts of newConditions that are set match what's in oldConditions (but +// doesn't look at anything else in oldConditions) +func conditionsInclude(oldConditions, newConditions []configv1.ClusterOperatorStatusCondition) bool { + for _, newCondition := range newConditions { + foundMatchingCondition := false + + for _, oldCondition := range oldConditions { + if newCondition.Type != oldCondition.Type || newCondition.Status != oldCondition.Status { + continue + } + if newCondition.Reason != "" && newCondition.Reason != oldCondition.Reason { + return false + } + if newCondition.Message != "" && newCondition.Message != oldCondition.Message { + return false + } + foundMatchingCondition = true + break + } + + if !foundMatchingCondition { + return false + } + } + + return true +}