Skip to content

Commit 92eea8f

Browse files
committed
Added E2E testcase for ACME DNS-01 challenge
1 parent 4b2fc9d commit 92eea8f

File tree

2 files changed

+447
-23
lines changed

2 files changed

+447
-23
lines changed

test/e2e/certman_operator_tests.go

Lines changed: 273 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,19 @@ package osde2etests
66

77
import (
88
"context"
9-
"crypto/x509"
109
"encoding/json"
11-
"encoding/pem"
1210
"fmt"
1311
"strings"
1412
"time"
1513

14+
"github.com/aws/aws-sdk-go/service/route53"
1615
"github.com/onsi/ginkgo/v2"
1716
"github.com/onsi/gomega"
1817
utils "github.com/openshift/certman-operator/test/e2e/utils"
1918
configv1 "github.com/openshift/client-go/config/clientset/versioned/typed/config/v1"
2019
"github.com/openshift/osde2e-common/pkg/clients/openshift"
2120
corev1 "k8s.io/api/core/v1"
2221
apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
23-
"k8s.io/apimachinery/pkg/api/errors"
2422
apierrors "k8s.io/apimachinery/pkg/api/errors"
2523
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2624

@@ -55,13 +53,12 @@ var _ = ginkgo.Describe("Certman Operator", ginkgo.Ordered, ginkgo.ContinueOnFai
5553
)
5654

5755
const (
58-
pollingDuration = 5 * time.Minute
59-
shortTimeout = 5 * time.Minute
60-
testTimeout = 10 * time.Minute
61-
namespace = "openshift-config"
62-
namespace_certman_operator = "certman-operator"
63-
operatorNS = "certman-operator"
64-
awsSecretName = "aws"
56+
pollingDuration = 5 * time.Minute
57+
shortTimeout = 5 * time.Minute
58+
testTimeout = 10 * time.Minute
59+
namespace = "openshift-config"
60+
operatorNS = "certman-operator"
61+
awsSecretName = "aws"
6562
)
6663

6764
ginkgo.BeforeAll(func(ctx context.Context) {
@@ -550,6 +547,113 @@ var _ = ginkgo.Describe("Certman Operator", ginkgo.Ordered, ginkgo.ContinueOnFai
550547
log.Log.Info("Test completed: CertificateRequest recreated by operator", "newCR", newCRName, "certificateSecret", secretNameFromCR)
551548
})
552549

550+
ginkgo.It("should create TXT records in Route53 for DNS-01 challenge", func(ctx context.Context) {
551+
// Find the DNSZone for our cluster
552+
dnsZone, err := utils.FindDNSZoneForClusterDeployment(ctx, dynamicClient,
553+
certConfig.TestNamespace, clusterDeploymentName)
554+
gomega.Expect(err).ShouldNot(gomega.HaveOccurred(), "DNSZone should exist for ClusterDeployment")
555+
556+
// Verify DNSZone is ready before querying Route53
557+
gomega.Eventually(func() bool {
558+
// Re-fetch DNSZone to get latest status
559+
freshDNSZone, err := utils.FindDNSZoneForClusterDeployment(ctx, dynamicClient,
560+
certConfig.TestNamespace, clusterDeploymentName)
561+
if err != nil {
562+
return false
563+
}
564+
ready, err := utils.VerifyDNSZoneReady(freshDNSZone)
565+
if err != nil {
566+
ginkgo.GinkgoLogr.Error(err, "Error checking DNSZone readiness")
567+
return false
568+
}
569+
if !ready {
570+
ginkgo.GinkgoLogr.Info("DNSZone not ready yet, waiting...")
571+
} else {
572+
// Update dnsZone with fresh data when ready
573+
dnsZone = freshDNSZone
574+
}
575+
return ready
576+
}, 5*time.Minute, 10*time.Second).Should(gomega.BeTrue(), "DNSZone should be ready")
577+
578+
// Get hosted zone ID from DNSZone status
579+
hostedZoneID, err := utils.GetHostedZoneIDFromDNSZone(dnsZone)
580+
gomega.Expect(err).ShouldNot(gomega.HaveOccurred(), "Should extract zone ID from DNSZone")
581+
582+
ginkgo.GinkgoLogr.Info("Testing Route53 DNS records",
583+
"hostedZoneID", hostedZoneID,
584+
"namespace", certConfig.TestNamespace)
585+
586+
// Create Route53 client
587+
route53Client, err := utils.CreateRoute53Client()
588+
gomega.Expect(err).ShouldNot(gomega.HaveOccurred(), "Should create Route53 client")
589+
590+
// Find the CertificateRequest
591+
certificateRequest, err := utils.FindCertificateRequestForClusterDeployment(ctx, dynamicClient,
592+
certificateRequestGVR, certConfig.TestNamespace, clusterDeploymentName)
593+
gomega.Expect(err).ShouldNot(gomega.HaveOccurred(), "CertificateRequest should exist")
594+
595+
// Get expected DNS names
596+
dnsNames, found, err := unstructured.NestedStringSlice(certificateRequest.Object, "spec", "dnsNames")
597+
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
598+
gomega.Expect(found).To(gomega.BeTrue(), "CertificateRequest should have dnsNames")
599+
gomega.Expect(len(dnsNames)).To(gomega.BeNumerically(">", 0), "Should have at least one DNS name")
600+
601+
ginkgo.GinkgoLogr.Info("Expected DNS names", "dnsNames", dnsNames)
602+
603+
// Wait for TXT records to be created in Route53
604+
var challengeRecords []*route53.ResourceRecordSet
605+
gomega.Eventually(func() bool {
606+
records, err := utils.ListAcmeChallengeTXTRecords(route53Client, hostedZoneID)
607+
if err != nil {
608+
ginkgo.GinkgoLogr.Error(err, "Failed to list Route53 records")
609+
return false
610+
}
611+
612+
if len(records) == 0 {
613+
ginkgo.GinkgoLogr.Info("No _acme-challenge TXT records found yet, waiting...")
614+
return false
615+
}
616+
617+
challengeRecords = records
618+
ginkgo.GinkgoLogr.Info("Found _acme-challenge TXT records",
619+
"count", len(records))
620+
621+
// Verify we have records for our domains
622+
for _, record := range records {
623+
ginkgo.GinkgoLogr.Info("Found TXT record",
624+
"name", *record.Name,
625+
"type", *record.Type,
626+
"ttl", *record.TTL)
627+
}
628+
629+
return true
630+
}, 10*time.Minute, 15*time.Second).Should(gomega.BeTrue(),
631+
"TXT records should be created in Route53 for DNS-01 challenge")
632+
633+
// Validate record format
634+
for _, record := range challengeRecords {
635+
// Verify it's a TXT record
636+
gomega.Expect(*record.Type).To(gomega.Equal("TXT"), "Record should be TXT type")
637+
638+
// Verify name starts with _acme-challenge
639+
gomega.Expect(*record.Name).To(gomega.ContainSubstring("_acme-challenge"),
640+
"Record name should contain _acme-challenge")
641+
642+
// Verify TTL is reasonable (usually 60 seconds for ACME challenges)
643+
gomega.Expect(*record.TTL).To(gomega.BeNumerically("<=", 300),
644+
"TTL should be short for challenge records")
645+
646+
// Verify record has a value
647+
gomega.Expect(len(record.ResourceRecords)).To(gomega.BeNumerically(">", 0),
648+
"TXT record should have a value")
649+
650+
ginkgo.GinkgoLogr.Info("✅ TXT record validated",
651+
"name", *record.Name,
652+
"ttl", *record.TTL,
653+
"value", *record.ResourceRecords[0].Value)
654+
}
655+
})
656+
553657
ginkgo.It("should verify primary-cert-bundle-secret and certificate creation", func(ctx context.Context) {
554658
// Find the CertificateRequest for our ClusterDeployment
555659
certificateRequest, err := utils.FindCertificateRequestForClusterDeployment(ctx, dynamicClient, certificateRequestGVR,
@@ -612,14 +716,165 @@ var _ = ginkgo.Describe("Certman Operator", ginkgo.Ordered, ginkgo.ContinueOnFai
612716
}, testTimeout, 15*time.Second).Should(gomega.BeTrue(), "primary-cert-bundle-secret should be created with certificate data")
613717

614718
// Verify certificate is valid
615-
block, _ := pem.Decode(secret.Data["tls.crt"])
616-
gomega.Expect(block).ToNot(gomega.BeNil(), "Certificate should be valid PEM")
617-
cert, err := x509.ParseCertificate(block.Bytes)
719+
cert, err := utils.ParseCertificateFromSecret(secret)
618720
gomega.Expect(err).ShouldNot(gomega.HaveOccurred(), "Certificate should be parseable")
619721

722+
// Verify certificate is not expired and currently valid
723+
err = utils.VerifyCertificateExpiry(cert)
724+
gomega.Expect(err).ShouldNot(gomega.HaveOccurred(), "Certificate should be currently valid")
725+
726+
// Get remaining days until expiry
727+
remainingDays := utils.GetCertificateRemainingDays(cert)
728+
gomega.Expect(remainingDays).To(gomega.BeNumerically(">", 0),
729+
"Certificate should have positive remaining days")
730+
731+
// Get expected DNS names from CertificateRequest spec
732+
expectedDNSNames, found, err := unstructured.NestedStringSlice(certificateRequest.Object, "spec", "dnsNames")
733+
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
734+
gomega.Expect(found).To(gomega.BeTrue())
735+
736+
// Verify certificate has at least 2 DNS names (api and apps)
737+
gomega.Expect(len(cert.DNSNames)).To(gomega.BeNumerically(">=", 2),
738+
"Certificate should have at least 2 DNS names (api and apps)")
739+
740+
// Verify certificate has all expected DNS names
741+
for _, expectedDNS := range expectedDNSNames {
742+
found := false
743+
for _, certDNS := range cert.DNSNames {
744+
if certDNS == expectedDNS {
745+
found = true
746+
break
747+
}
748+
}
749+
gomega.Expect(found).To(gomega.BeTrue(),
750+
fmt.Sprintf("Certificate should contain DNS name: %s", expectedDNS))
751+
}
752+
620753
ginkgo.GinkgoLogr.Info("Certificate and primary-cert-bundle-secret verified successfully",
621754
"secretName", certificateSecretName,
622-
"dnsNames", cert.DNSNames)
755+
"dnsNames", cert.DNSNames,
756+
"NotBefore", cert.NotBefore,
757+
"NotAfter", cert.NotAfter,
758+
"RemainingDays", remainingDays)
759+
})
760+
761+
ginkgo.It("should delete TXT records from Route53 after certificate issuance", func(ctx context.Context) {
762+
// Find the DNSZone
763+
dnsZone, err := utils.FindDNSZoneForClusterDeployment(ctx, dynamicClient,
764+
certConfig.TestNamespace, clusterDeploymentName)
765+
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
766+
767+
// Verify DNSZone is ready
768+
gomega.Eventually(func() bool {
769+
freshDNSZone, err := utils.FindDNSZoneForClusterDeployment(ctx, dynamicClient,
770+
certConfig.TestNamespace, clusterDeploymentName)
771+
if err != nil {
772+
return false
773+
}
774+
ready, err := utils.VerifyDNSZoneReady(freshDNSZone)
775+
if err != nil {
776+
return false
777+
}
778+
if ready {
779+
dnsZone = freshDNSZone
780+
}
781+
return ready
782+
}, 5*time.Minute, 10*time.Second).Should(gomega.BeTrue(), "DNSZone should be ready")
783+
784+
// Get hosted zone ID
785+
hostedZoneID, err := utils.GetHostedZoneIDFromDNSZone(dnsZone)
786+
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
787+
788+
// Create Route53 client
789+
route53Client, err := utils.CreateRoute53Client()
790+
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
791+
792+
// Wait for certificate to be issued
793+
gomega.Eventually(func() bool {
794+
certificateRequest, err := utils.FindCertificateRequestForClusterDeployment(ctx, dynamicClient,
795+
certificateRequestGVR, certConfig.TestNamespace, clusterDeploymentName)
796+
if err != nil {
797+
return false
798+
}
799+
800+
issued, found, _ := unstructured.NestedBool(certificateRequest.Object, "status", "issued")
801+
if !found {
802+
return false
803+
}
804+
805+
if issued {
806+
ginkgo.GinkgoLogr.Info("✅ Certificate issued, checking for TXT record cleanup...")
807+
}
808+
809+
return issued
810+
}, 15*time.Minute, 15*time.Second).Should(gomega.BeTrue(),
811+
"Certificate should be issued before checking cleanup")
812+
813+
// Verify TXT records are DELETED after certificate issuance
814+
gomega.Eventually(func() bool {
815+
records, err := utils.ListAcmeChallengeTXTRecords(route53Client, hostedZoneID)
816+
if err != nil {
817+
ginkgo.GinkgoLogr.Error(err, "Failed to list Route53 records")
818+
return false
819+
}
820+
821+
if len(records) > 0 {
822+
ginkgo.GinkgoLogr.Info("Challenge TXT records still exist, waiting for cleanup...",
823+
"count", len(records))
824+
for _, record := range records {
825+
ginkgo.GinkgoLogr.Info("Orphaned record",
826+
"name", *record.Name)
827+
}
828+
return false
829+
}
830+
831+
ginkgo.GinkgoLogr.Info("✅ All _acme-challenge TXT records cleaned up")
832+
return true
833+
}, 5*time.Minute, 10*time.Second).Should(gomega.BeTrue(),
834+
"TXT records should be deleted from Route53 after certificate issuance")
835+
836+
// Test certificate renewal by deleting and recreating the secret
837+
ginkgo.GinkgoLogr.Info("Testing certificate renewal simulation")
838+
839+
certificateRequest, err := utils.FindCertificateRequestForClusterDeployment(ctx, dynamicClient,
840+
certificateRequestGVR, certConfig.TestNamespace, clusterDeploymentName)
841+
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
842+
843+
certificateSecretName, err := utils.GetCertificateSecretNameFromCR(certificateRequest)
844+
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
845+
846+
// Get original secret creation time
847+
originalSecret, err := clientset.CoreV1().Secrets(certConfig.TestNamespace).Get(ctx, certificateSecretName, metav1.GetOptions{})
848+
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
849+
850+
originalCreationTime := originalSecret.CreationTimestamp.Time
851+
852+
// Delete certificate secret to simulate renewal
853+
ginkgo.GinkgoLogr.Info("Deleting certificate secret to simulate renewal", "secret", certificateSecretName)
854+
err = clientset.CoreV1().Secrets(certConfig.TestNamespace).Delete(ctx, certificateSecretName, metav1.DeleteOptions{})
855+
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
856+
857+
// Wait for secret to be recreated
858+
gomega.Eventually(func() bool {
859+
newSecret, err := clientset.CoreV1().Secrets(certConfig.TestNamespace).Get(ctx, certificateSecretName, metav1.GetOptions{})
860+
if err != nil || len(newSecret.Data["tls.crt"]) == 0 {
861+
return false
862+
}
863+
return newSecret.CreationTimestamp.Time.After(originalCreationTime)
864+
}, 10*time.Minute, 15*time.Second).Should(gomega.BeTrue(),
865+
"Certificate secret should be recreated after deletion (renewal)")
866+
867+
// Verify renewed certificate is valid
868+
renewedSecret, err := clientset.CoreV1().Secrets(certConfig.TestNamespace).Get(ctx, certificateSecretName, metav1.GetOptions{})
869+
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
870+
871+
renewedCert, err := utils.ParseCertificateFromSecret(renewedSecret)
872+
gomega.Expect(err).ShouldNot(gomega.HaveOccurred())
873+
874+
err = utils.VerifyCertificateExpiry(renewedCert)
875+
gomega.Expect(err).ShouldNot(gomega.HaveOccurred(), "Renewed certificate should be valid")
876+
877+
ginkgo.GinkgoLogr.Info("✅ Certificate renewal simulation completed successfully")
623878
})
624879

625880
ginkgo.It("should verify certificate operation metrics", func(ctx context.Context) {
@@ -706,8 +961,8 @@ var _ = ginkgo.Describe("Certman Operator", ginkgo.Ordered, ginkgo.ContinueOnFai
706961
pollingDuration := 2 * time.Minute
707962
pollInterval := 30 * time.Second
708963

709-
originalSecret, err := clientset.CoreV1().Secrets(namespace_certman_operator).Get(ctx, secretNameToDelete, metav1.GetOptions{})
710-
if errors.IsNotFound(err) {
964+
originalSecret, err := clientset.CoreV1().Secrets(operatorNS).Get(ctx, secretNameToDelete, metav1.GetOptions{})
965+
if apierrors.IsNotFound(err) {
711966
log.Log.Info("Secret does not exist, skipping deletion test.")
712967
return
713968
}
@@ -716,11 +971,11 @@ var _ = ginkgo.Describe("Certman Operator", ginkgo.Ordered, ginkgo.ContinueOnFai
716971
originalTimestamp := originalSecret.CreationTimestamp.Time
717972
log.Log.Info(fmt.Sprintf("Original secret creation timestamp: %v", originalTimestamp))
718973

719-
err = clientset.CoreV1().Secrets(namespace_certman_operator).Delete(ctx, secretNameToDelete, metav1.DeleteOptions{})
974+
err = clientset.CoreV1().Secrets(operatorNS).Delete(ctx, secretNameToDelete, metav1.DeleteOptions{})
720975
gomega.Expect(err).ShouldNot(gomega.HaveOccurred(), "Failed to delete the secret")
721976

722977
gomega.Eventually(func() bool {
723-
newSecret, err := clientset.CoreV1().Secrets(namespace_certman_operator).Get(ctx, secretNameToDelete, metav1.GetOptions{})
978+
newSecret, err := clientset.CoreV1().Secrets(operatorNS).Get(ctx, secretNameToDelete, metav1.GetOptions{})
724979
if err != nil {
725980
return false
726981
}

0 commit comments

Comments
 (0)