@@ -6,21 +6,19 @@ package osde2etests
66
77import (
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