Skip to content

Commit 868580a

Browse files
committed
Add unit tests for cert signer
Signed-off-by: Periyasamy Palanisamy <[email protected]>
1 parent 4dc8dc8 commit 868580a

File tree

1 file changed

+273
-0
lines changed

1 file changed

+273
-0
lines changed

pkg/controller/signer/signer_test.go

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
package signer
2+
3+
import (
4+
"context"
5+
"crypto/rand"
6+
"crypto/rsa"
7+
"crypto/x509"
8+
"crypto/x509/pkix"
9+
"encoding/pem"
10+
"fmt"
11+
"math/big"
12+
"testing"
13+
"time"
14+
15+
. "github.com/onsi/gomega"
16+
configv1 "github.com/openshift/api/config/v1"
17+
operv1 "github.com/openshift/api/operator/v1"
18+
cnoclient "github.com/openshift/cluster-network-operator/pkg/client"
19+
"github.com/openshift/cluster-network-operator/pkg/client/fake"
20+
"github.com/openshift/cluster-network-operator/pkg/controller/statusmanager"
21+
"github.com/openshift/cluster-network-operator/pkg/names"
22+
certificatev1 "k8s.io/api/certificates/v1"
23+
corev1 "k8s.io/api/core/v1"
24+
apierrors "k8s.io/apimachinery/pkg/api/errors"
25+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26+
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27+
"k8s.io/apimachinery/pkg/types"
28+
"k8s.io/client-go/kubernetes/scheme"
29+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
30+
)
31+
32+
var (
33+
csrName = "ipsec-csr"
34+
nodeName = "testnode"
35+
coName = "testing"
36+
)
37+
38+
//nolint:errcheck
39+
func init() {
40+
certificatev1.AddToScheme(scheme.Scheme)
41+
corev1.AddToScheme(scheme.Scheme)
42+
}
43+
44+
func TestSigner_reconciler(t *testing.T) {
45+
g := NewGomegaWithT(t)
46+
client := fake.NewFakeClient()
47+
status := statusmanager.New(client, coName, names.StandAloneClusterName)
48+
signer := ReconcileCSR{client: client, status: status}
49+
50+
co := &configv1.ClusterOperator{ObjectMeta: metav1.ObjectMeta{Name: coName}}
51+
setCO(t, client, co)
52+
no := &operv1.Network{ObjectMeta: metav1.ObjectMeta{Name: names.OPERATOR_CONFIG}}
53+
setOC(t, client, no)
54+
55+
csr, err := generateCSR()
56+
g.Expect(err).NotTo(HaveOccurred())
57+
csrObj := &certificatev1.CertificateSigningRequest{}
58+
csrObj.Name = csrName
59+
csrObj.Spec.Request = []byte(csr)
60+
csrObj.Spec.SignerName = signerName
61+
csrObj.Spec.Usages = []certificatev1.KeyUsage{"ipsec tunnel"}
62+
csrObj.Spec.Username = fmt.Sprintf("system:ovn-node:%s", nodeName)
63+
csrObj.Status.Conditions = append(csrObj.Status.Conditions, certificatev1.CertificateSigningRequestCondition{
64+
Type: certificatev1.CertificateApproved,
65+
Status: "True",
66+
Reason: "AutoApproved",
67+
Message: "Automatically approved by " + signerName})
68+
69+
err = client.Default().CRClient().Create(context.TODO(), csrObj)
70+
g.Expect(err).NotTo(HaveOccurred())
71+
_, err = client.Default().Kubernetes().CertificatesV1().CertificateSigningRequests().Create(context.TODO(), csrObj, v1.CreateOptions{})
72+
g.Expect(err).NotTo(HaveOccurred())
73+
74+
node := &corev1.Node{}
75+
node.Name = nodeName
76+
_, err = client.Default().Kubernetes().CoreV1().Nodes().Create(context.TODO(), node, v1.CreateOptions{})
77+
g.Expect(err).NotTo(HaveOccurred())
78+
79+
caKey, caCert, err := generateSelfSignedCACertificate()
80+
g.Expect(err).NotTo(HaveOccurred())
81+
caSecret := &corev1.Secret{}
82+
caSecret.Name = "signer-ca"
83+
caSecret.Namespace = "openshift-ovn-kubernetes"
84+
caSecret.Data = make(map[string][]byte)
85+
caSecret.Data["tls.crt"] = []byte(caCert)
86+
caSecret.Data["tls.key"] = []byte(caKey)
87+
err = client.Default().CRClient().Create(context.TODO(), caSecret)
88+
g.Expect(err).NotTo(HaveOccurred())
89+
90+
_, err = signer.Reconcile(context.TODO(),
91+
reconcile.Request{NamespacedName: types.NamespacedName{Name: csrName}})
92+
g.Expect(err).NotTo(HaveOccurred())
93+
94+
err = client.Default().CRClient().Get(context.TODO(), types.NamespacedName{Name: csrName}, csrObj)
95+
g.Expect(err).NotTo(HaveOccurred())
96+
g.Expect(csrObj.Status.Certificate).ShouldNot(BeEmpty())
97+
98+
co, _, err = getStatuses(client, "testing")
99+
if err != nil {
100+
t.Fatalf("error getting network.operator: %v", err)
101+
}
102+
g.Expect(err).NotTo(HaveOccurred())
103+
g.Expect(conditionsInclude(co.Status.Conditions, []configv1.ClusterOperatorStatusCondition{
104+
{
105+
Type: configv1.OperatorDegraded,
106+
Status: configv1.ConditionFalse,
107+
},
108+
})).To(BeTrue())
109+
g.Expect(conditionsInclude(co.Status.Conditions, []configv1.ClusterOperatorStatusCondition{
110+
{
111+
Type: configv1.OperatorUpgradeable,
112+
Status: configv1.ConditionTrue,
113+
},
114+
})).To(BeTrue())
115+
}
116+
117+
func TestSigner_reconciler_withInvalidUserName(t *testing.T) {
118+
g := NewGomegaWithT(t)
119+
client := fake.NewFakeClient()
120+
status := statusmanager.New(client, coName, names.StandAloneClusterName)
121+
signer := ReconcileCSR{client: client, status: status}
122+
123+
co := &configv1.ClusterOperator{ObjectMeta: metav1.ObjectMeta{Name: coName}}
124+
setCO(t, client, co)
125+
no := &operv1.Network{ObjectMeta: metav1.ObjectMeta{Name: names.OPERATOR_CONFIG}}
126+
setOC(t, client, no)
127+
128+
csr, err := generateCSR()
129+
g.Expect(err).NotTo(HaveOccurred())
130+
csrObj := &certificatev1.CertificateSigningRequest{}
131+
csrObj.Name = csrName
132+
csrObj.Spec.Request = []byte(csr)
133+
csrObj.Spec.SignerName = signerName
134+
csrObj.Spec.Usages = []certificatev1.KeyUsage{"ipsec tunnel"}
135+
csrObj.Spec.Username = fmt.Sprintf("system:ovn-node:%s", "suspicious-node")
136+
137+
err = client.Default().CRClient().Create(context.TODO(), csrObj)
138+
g.Expect(err).NotTo(HaveOccurred())
139+
_, err = client.Default().Kubernetes().CertificatesV1().CertificateSigningRequests().Create(context.TODO(), csrObj, v1.CreateOptions{})
140+
g.Expect(err).NotTo(HaveOccurred())
141+
142+
node := &corev1.Node{}
143+
node.Name = nodeName
144+
_, err = client.Default().Kubernetes().CoreV1().Nodes().Create(context.TODO(), node, v1.CreateOptions{})
145+
g.Expect(err).NotTo(HaveOccurred())
146+
147+
_, err = signer.Reconcile(context.TODO(),
148+
reconcile.Request{NamespacedName: types.NamespacedName{Name: csrName}})
149+
g.Expect(err).NotTo(HaveOccurred())
150+
151+
err = client.Default().CRClient().Get(context.TODO(), types.NamespacedName{Name: csrName}, csrObj)
152+
g.Expect(err).NotTo(HaveOccurred())
153+
g.Expect(csrObj.Status.Certificate).Should(BeEmpty())
154+
csrConditions := csrObj.Status.Conditions
155+
g.Expect(len(csrConditions)).To(Equal(1))
156+
g.Expect(csrConditions[0].Reason).To(Equal("CSRInvalidUser"))
157+
g.Expect(csrConditions[0].Type).To(Equal(certificatev1.CertificateFailed))
158+
}
159+
160+
func generateSelfSignedCACertificate() (string, string, error) {
161+
// Create private key.
162+
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
163+
if err != nil {
164+
return "", "", fmt.Errorf("failed to generate private key: %v", err)
165+
}
166+
// Create a certificate template for CA certificate.
167+
certTemplate := x509.Certificate{
168+
SerialNumber: big.NewInt(1),
169+
Subject: pkix.Name{
170+
Organization: []string{"OpenShift"},
171+
CommonName: signerName,
172+
},
173+
NotBefore: time.Now(),
174+
NotAfter: time.Now().Add(10 * time.Minute),
175+
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature,
176+
BasicConstraintsValid: true,
177+
IsCA: true,
178+
MaxPathLen: 0,
179+
}
180+
// Self-sign the certificate using the private key.
181+
certDER, err := x509.CreateCertificate(rand.Reader, &certTemplate, &certTemplate, &privateKey.PublicKey, privateKey)
182+
if err != nil {
183+
return "", "", fmt.Errorf("failed to create ca certificate: %v", err)
184+
}
185+
// Encode CA private key in PEM format.
186+
privateKeyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)})
187+
if privateKeyPEM == nil {
188+
return "", "", fmt.Errorf("failed to encode private key in PEM format")
189+
}
190+
// Encode CA certificate in PEM format.
191+
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
192+
if certPEM == nil {
193+
return "", "", fmt.Errorf("failed to encode certificate in PEM format")
194+
}
195+
return string(privateKeyPEM), string(certPEM), nil
196+
}
197+
198+
func generateCSR() (string, error) {
199+
// Create private key.
200+
csrKey, err := rsa.GenerateKey(rand.Reader, 2048)
201+
if err != nil {
202+
return "", fmt.Errorf("failed to generate private key: %v", err)
203+
}
204+
// Create CSR with private key.
205+
csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{}, csrKey)
206+
if err != nil {
207+
return "", err
208+
}
209+
// Encode CSR in PEM format.
210+
csrPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrBytes})
211+
if csrPEM == nil {
212+
return "", fmt.Errorf("failed to encode CSR in PEM format")
213+
}
214+
return string(csrPEM), nil
215+
}
216+
217+
func setOC(t *testing.T, client cnoclient.Client, oc *operv1.Network) {
218+
t.Helper()
219+
g := NewGomegaWithT(t)
220+
_, err := client.Default().OpenshiftOperatorClient().OperatorV1().Networks().Update(context.TODO(), oc, metav1.UpdateOptions{})
221+
if apierrors.IsNotFound(err) {
222+
_, err = client.Default().OpenshiftOperatorClient().OperatorV1().Networks().Create(context.TODO(), oc, metav1.CreateOptions{})
223+
}
224+
g.Expect(err).NotTo(HaveOccurred())
225+
}
226+
227+
func setCO(t *testing.T, client cnoclient.Client, co *configv1.ClusterOperator) {
228+
t.Helper()
229+
g := NewGomegaWithT(t)
230+
err := client.Default().CRClient().Update(context.TODO(), co)
231+
if apierrors.IsNotFound(err) {
232+
err = client.Default().CRClient().Create(context.TODO(), co)
233+
}
234+
g.Expect(err).NotTo(HaveOccurred())
235+
}
236+
237+
func getStatuses(client cnoclient.Client, name string) (*configv1.ClusterOperator, *operv1.Network, error) {
238+
co := &configv1.ClusterOperator{ObjectMeta: metav1.ObjectMeta{Name: name}}
239+
err := client.ClientFor("").CRClient().Get(context.TODO(), types.NamespacedName{Name: name}, co)
240+
if err != nil {
241+
return nil, nil, err
242+
}
243+
oc, err := client.Default().OpenshiftOperatorClient().OperatorV1().Networks().Get(context.TODO(), names.OPERATOR_CONFIG, metav1.GetOptions{})
244+
return co, oc, err
245+
}
246+
247+
// Tests that the parts of newConditions that are set match what's in oldConditions (but
248+
// doesn't look at anything else in oldConditions)
249+
func conditionsInclude(oldConditions, newConditions []configv1.ClusterOperatorStatusCondition) bool {
250+
for _, newCondition := range newConditions {
251+
foundMatchingCondition := false
252+
253+
for _, oldCondition := range oldConditions {
254+
if newCondition.Type != oldCondition.Type || newCondition.Status != oldCondition.Status {
255+
continue
256+
}
257+
if newCondition.Reason != "" && newCondition.Reason != oldCondition.Reason {
258+
return false
259+
}
260+
if newCondition.Message != "" && newCondition.Message != oldCondition.Message {
261+
return false
262+
}
263+
foundMatchingCondition = true
264+
break
265+
}
266+
267+
if !foundMatchingCondition {
268+
return false
269+
}
270+
}
271+
272+
return true
273+
}

0 commit comments

Comments
 (0)