@@ -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 ) {
@@ -443,6 +440,113 @@ var _ = ginkgo.Describe("Certman Operator", ginkgo.Ordered, ginkgo.ContinueOnFai
443440 "crName" , foundCertificateRequest .GetName ())
444441 })
445442
443+ ginkgo .It ("should create TXT records in Route53 for DNS-01 challenge" , func (ctx context.Context ) {
444+ // Find the DNSZone for our cluster
445+ dnsZone , err := utils .FindDNSZoneForClusterDeployment (ctx , dynamicClient ,
446+ certConfig .TestNamespace , clusterDeploymentName )
447+ gomega .Expect (err ).ShouldNot (gomega .HaveOccurred (), "DNSZone should exist for ClusterDeployment" )
448+
449+ // Verify DNSZone is ready before querying Route53
450+ gomega .Eventually (func () bool {
451+ // Re-fetch DNSZone to get latest status
452+ freshDNSZone , err := utils .FindDNSZoneForClusterDeployment (ctx , dynamicClient ,
453+ certConfig .TestNamespace , clusterDeploymentName )
454+ if err != nil {
455+ return false
456+ }
457+ ready , err := utils .VerifyDNSZoneReady (freshDNSZone )
458+ if err != nil {
459+ ginkgo .GinkgoLogr .Error (err , "Error checking DNSZone readiness" )
460+ return false
461+ }
462+ if ! ready {
463+ ginkgo .GinkgoLogr .Info ("DNSZone not ready yet, waiting..." )
464+ } else {
465+ // Update dnsZone with fresh data when ready
466+ dnsZone = freshDNSZone
467+ }
468+ return ready
469+ }, 5 * time .Minute , 10 * time .Second ).Should (gomega .BeTrue (), "DNSZone should be ready" )
470+
471+ // Get hosted zone ID from DNSZone status
472+ hostedZoneID , err := utils .GetHostedZoneIDFromDNSZone (dnsZone )
473+ gomega .Expect (err ).ShouldNot (gomega .HaveOccurred (), "Should extract zone ID from DNSZone" )
474+
475+ ginkgo .GinkgoLogr .Info ("Testing Route53 DNS records" ,
476+ "hostedZoneID" , hostedZoneID ,
477+ "namespace" , certConfig .TestNamespace )
478+
479+ // Create Route53 client
480+ route53Client , err := utils .CreateRoute53Client ()
481+ gomega .Expect (err ).ShouldNot (gomega .HaveOccurred (), "Should create Route53 client" )
482+
483+ // Find the CertificateRequest
484+ certificateRequest , err := utils .FindCertificateRequestForClusterDeployment (ctx , dynamicClient ,
485+ certificateRequestGVR , certConfig .TestNamespace , clusterDeploymentName )
486+ gomega .Expect (err ).ShouldNot (gomega .HaveOccurred (), "CertificateRequest should exist" )
487+
488+ // Get expected DNS names
489+ dnsNames , found , err := unstructured .NestedStringSlice (certificateRequest .Object , "spec" , "dnsNames" )
490+ gomega .Expect (err ).ShouldNot (gomega .HaveOccurred ())
491+ gomega .Expect (found ).To (gomega .BeTrue (), "CertificateRequest should have dnsNames" )
492+ gomega .Expect (len (dnsNames )).To (gomega .BeNumerically (">" , 0 ), "Should have at least one DNS name" )
493+
494+ ginkgo .GinkgoLogr .Info ("Expected DNS names" , "dnsNames" , dnsNames )
495+
496+ // Wait for TXT records to be created in Route53
497+ var challengeRecords []* route53.ResourceRecordSet
498+ gomega .Eventually (func () bool {
499+ records , err := utils .ListAcmeChallengeTXTRecords (route53Client , hostedZoneID )
500+ if err != nil {
501+ ginkgo .GinkgoLogr .Error (err , "Failed to list Route53 records" )
502+ return false
503+ }
504+
505+ if len (records ) == 0 {
506+ ginkgo .GinkgoLogr .Info ("No _acme-challenge TXT records found yet, waiting..." )
507+ return false
508+ }
509+
510+ challengeRecords = records
511+ ginkgo .GinkgoLogr .Info ("Found _acme-challenge TXT records" ,
512+ "count" , len (records ))
513+
514+ // Verify we have records for our domains
515+ for _ , record := range records {
516+ ginkgo .GinkgoLogr .Info ("Found TXT record" ,
517+ "name" , * record .Name ,
518+ "type" , * record .Type ,
519+ "ttl" , * record .TTL )
520+ }
521+
522+ return true
523+ }, 10 * time .Minute , 15 * time .Second ).Should (gomega .BeTrue (),
524+ "TXT records should be created in Route53 for DNS-01 challenge" )
525+
526+ // Validate record format
527+ for _ , record := range challengeRecords {
528+ // Verify it's a TXT record
529+ gomega .Expect (* record .Type ).To (gomega .Equal ("TXT" ), "Record should be TXT type" )
530+
531+ // Verify name starts with _acme-challenge
532+ gomega .Expect (* record .Name ).To (gomega .ContainSubstring ("_acme-challenge" ),
533+ "Record name should contain _acme-challenge" )
534+
535+ // Verify TTL is reasonable (usually 60 seconds for ACME challenges)
536+ gomega .Expect (* record .TTL ).To (gomega .BeNumerically ("<=" , 300 ),
537+ "TTL should be short for challenge records" )
538+
539+ // Verify record has a value
540+ gomega .Expect (len (record .ResourceRecords )).To (gomega .BeNumerically (">" , 0 ),
541+ "TXT record should have a value" )
542+
543+ ginkgo .GinkgoLogr .Info ("✅ TXT record validated" ,
544+ "name" , * record .Name ,
545+ "ttl" , * record .TTL ,
546+ "value" , * record .ResourceRecords [0 ].Value )
547+ }
548+ })
549+
446550 ginkgo .It ("should verify primary-cert-bundle-secret and certificate creation" , func (ctx context.Context ) {
447551 // Find the CertificateRequest for our ClusterDeployment
448552 certificateRequest , err := utils .FindCertificateRequestForClusterDeployment (ctx , dynamicClient , certificateRequestGVR ,
@@ -505,14 +609,165 @@ var _ = ginkgo.Describe("Certman Operator", ginkgo.Ordered, ginkgo.ContinueOnFai
505609 }, testTimeout , 15 * time .Second ).Should (gomega .BeTrue (), "primary-cert-bundle-secret should be created with certificate data" )
506610
507611 // Verify certificate is valid
508- block , _ := pem .Decode (secret .Data ["tls.crt" ])
509- gomega .Expect (block ).ToNot (gomega .BeNil (), "Certificate should be valid PEM" )
510- cert , err := x509 .ParseCertificate (block .Bytes )
612+ cert , err := utils .ParseCertificateFromSecret (secret )
511613 gomega .Expect (err ).ShouldNot (gomega .HaveOccurred (), "Certificate should be parseable" )
512614
615+ // Verify certificate is not expired and currently valid
616+ err = utils .VerifyCertificateExpiry (cert )
617+ gomega .Expect (err ).ShouldNot (gomega .HaveOccurred (), "Certificate should be currently valid" )
618+
619+ // Get remaining days until expiry
620+ remainingDays := utils .GetCertificateRemainingDays (cert )
621+ gomega .Expect (remainingDays ).To (gomega .BeNumerically (">" , 0 ),
622+ "Certificate should have positive remaining days" )
623+
624+ // Get expected DNS names from CertificateRequest spec
625+ expectedDNSNames , found , err := unstructured .NestedStringSlice (certificateRequest .Object , "spec" , "dnsNames" )
626+ gomega .Expect (err ).ShouldNot (gomega .HaveOccurred ())
627+ gomega .Expect (found ).To (gomega .BeTrue ())
628+
629+ // Verify certificate has at least 2 DNS names (api and apps)
630+ gomega .Expect (len (cert .DNSNames )).To (gomega .BeNumerically (">=" , 2 ),
631+ "Certificate should have at least 2 DNS names (api and apps)" )
632+
633+ // Verify certificate has all expected DNS names
634+ for _ , expectedDNS := range expectedDNSNames {
635+ found := false
636+ for _ , certDNS := range cert .DNSNames {
637+ if certDNS == expectedDNS {
638+ found = true
639+ break
640+ }
641+ }
642+ gomega .Expect (found ).To (gomega .BeTrue (),
643+ fmt .Sprintf ("Certificate should contain DNS name: %s" , expectedDNS ))
644+ }
645+
513646 ginkgo .GinkgoLogr .Info ("Certificate and primary-cert-bundle-secret verified successfully" ,
514647 "secretName" , certificateSecretName ,
515- "dnsNames" , cert .DNSNames )
648+ "dnsNames" , cert .DNSNames ,
649+ "NotBefore" , cert .NotBefore ,
650+ "NotAfter" , cert .NotAfter ,
651+ "RemainingDays" , remainingDays )
652+ })
653+
654+ ginkgo .It ("should delete TXT records from Route53 after certificate issuance" , func (ctx context.Context ) {
655+ // Find the DNSZone
656+ dnsZone , err := utils .FindDNSZoneForClusterDeployment (ctx , dynamicClient ,
657+ certConfig .TestNamespace , clusterDeploymentName )
658+ gomega .Expect (err ).ShouldNot (gomega .HaveOccurred ())
659+
660+ // Verify DNSZone is ready
661+ gomega .Eventually (func () bool {
662+ freshDNSZone , err := utils .FindDNSZoneForClusterDeployment (ctx , dynamicClient ,
663+ certConfig .TestNamespace , clusterDeploymentName )
664+ if err != nil {
665+ return false
666+ }
667+ ready , err := utils .VerifyDNSZoneReady (freshDNSZone )
668+ if err != nil {
669+ return false
670+ }
671+ if ready {
672+ dnsZone = freshDNSZone
673+ }
674+ return ready
675+ }, 5 * time .Minute , 10 * time .Second ).Should (gomega .BeTrue (), "DNSZone should be ready" )
676+
677+ // Get hosted zone ID
678+ hostedZoneID , err := utils .GetHostedZoneIDFromDNSZone (dnsZone )
679+ gomega .Expect (err ).ShouldNot (gomega .HaveOccurred ())
680+
681+ // Create Route53 client
682+ route53Client , err := utils .CreateRoute53Client ()
683+ gomega .Expect (err ).ShouldNot (gomega .HaveOccurred ())
684+
685+ // Wait for certificate to be issued
686+ gomega .Eventually (func () bool {
687+ certificateRequest , err := utils .FindCertificateRequestForClusterDeployment (ctx , dynamicClient ,
688+ certificateRequestGVR , certConfig .TestNamespace , clusterDeploymentName )
689+ if err != nil {
690+ return false
691+ }
692+
693+ issued , found , _ := unstructured .NestedBool (certificateRequest .Object , "status" , "issued" )
694+ if ! found {
695+ return false
696+ }
697+
698+ if issued {
699+ ginkgo .GinkgoLogr .Info ("✅ Certificate issued, checking for TXT record cleanup..." )
700+ }
701+
702+ return issued
703+ }, 15 * time .Minute , 15 * time .Second ).Should (gomega .BeTrue (),
704+ "Certificate should be issued before checking cleanup" )
705+
706+ // Verify TXT records are DELETED after certificate issuance
707+ gomega .Eventually (func () bool {
708+ records , err := utils .ListAcmeChallengeTXTRecords (route53Client , hostedZoneID )
709+ if err != nil {
710+ ginkgo .GinkgoLogr .Error (err , "Failed to list Route53 records" )
711+ return false
712+ }
713+
714+ if len (records ) > 0 {
715+ ginkgo .GinkgoLogr .Info ("Challenge TXT records still exist, waiting for cleanup..." ,
716+ "count" , len (records ))
717+ for _ , record := range records {
718+ ginkgo .GinkgoLogr .Info ("Orphaned record" ,
719+ "name" , * record .Name )
720+ }
721+ return false
722+ }
723+
724+ ginkgo .GinkgoLogr .Info ("✅ All _acme-challenge TXT records cleaned up" )
725+ return true
726+ }, 5 * time .Minute , 10 * time .Second ).Should (gomega .BeTrue (),
727+ "TXT records should be deleted from Route53 after certificate issuance" )
728+
729+ // Test certificate renewal by deleting and recreating the secret
730+ ginkgo .GinkgoLogr .Info ("Testing certificate renewal simulation" )
731+
732+ certificateRequest , err := utils .FindCertificateRequestForClusterDeployment (ctx , dynamicClient ,
733+ certificateRequestGVR , certConfig .TestNamespace , clusterDeploymentName )
734+ gomega .Expect (err ).ShouldNot (gomega .HaveOccurred ())
735+
736+ certificateSecretName , err := utils .GetCertificateSecretNameFromCR (certificateRequest )
737+ gomega .Expect (err ).ShouldNot (gomega .HaveOccurred ())
738+
739+ // Get original secret creation time
740+ originalSecret , err := clientset .CoreV1 ().Secrets (certConfig .TestNamespace ).Get (ctx , certificateSecretName , metav1.GetOptions {})
741+ gomega .Expect (err ).ShouldNot (gomega .HaveOccurred ())
742+
743+ originalCreationTime := originalSecret .CreationTimestamp .Time
744+
745+ // Delete certificate secret to simulate renewal
746+ ginkgo .GinkgoLogr .Info ("Deleting certificate secret to simulate renewal" , "secret" , certificateSecretName )
747+ err = clientset .CoreV1 ().Secrets (certConfig .TestNamespace ).Delete (ctx , certificateSecretName , metav1.DeleteOptions {})
748+ gomega .Expect (err ).ShouldNot (gomega .HaveOccurred ())
749+
750+ // Wait for secret to be recreated
751+ gomega .Eventually (func () bool {
752+ newSecret , err := clientset .CoreV1 ().Secrets (certConfig .TestNamespace ).Get (ctx , certificateSecretName , metav1.GetOptions {})
753+ if err != nil || len (newSecret .Data ["tls.crt" ]) == 0 {
754+ return false
755+ }
756+ return newSecret .CreationTimestamp .Time .After (originalCreationTime )
757+ }, 10 * time .Minute , 15 * time .Second ).Should (gomega .BeTrue (),
758+ "Certificate secret should be recreated after deletion (renewal)" )
759+
760+ // Verify renewed certificate is valid
761+ renewedSecret , err := clientset .CoreV1 ().Secrets (certConfig .TestNamespace ).Get (ctx , certificateSecretName , metav1.GetOptions {})
762+ gomega .Expect (err ).ShouldNot (gomega .HaveOccurred ())
763+
764+ renewedCert , err := utils .ParseCertificateFromSecret (renewedSecret )
765+ gomega .Expect (err ).ShouldNot (gomega .HaveOccurred ())
766+
767+ err = utils .VerifyCertificateExpiry (renewedCert )
768+ gomega .Expect (err ).ShouldNot (gomega .HaveOccurred (), "Renewed certificate should be valid" )
769+
770+ ginkgo .GinkgoLogr .Info ("✅ Certificate renewal simulation completed successfully" )
516771 })
517772
518773 ginkgo .It ("should verify certificate operation metrics" , func (ctx context.Context ) {
@@ -599,8 +854,8 @@ var _ = ginkgo.Describe("Certman Operator", ginkgo.Ordered, ginkgo.ContinueOnFai
599854 pollingDuration := 2 * time .Minute
600855 pollInterval := 30 * time .Second
601856
602- originalSecret , err := clientset .CoreV1 ().Secrets (namespace_certman_operator ).Get (ctx , secretNameToDelete , metav1.GetOptions {})
603- if errors .IsNotFound (err ) {
857+ originalSecret , err := clientset .CoreV1 ().Secrets (operatorNS ).Get (ctx , secretNameToDelete , metav1.GetOptions {})
858+ if apierrors .IsNotFound (err ) {
604859 log .Log .Info ("Secret does not exist, skipping deletion test." )
605860 return
606861 }
@@ -609,11 +864,11 @@ var _ = ginkgo.Describe("Certman Operator", ginkgo.Ordered, ginkgo.ContinueOnFai
609864 originalTimestamp := originalSecret .CreationTimestamp .Time
610865 log .Log .Info (fmt .Sprintf ("Original secret creation timestamp: %v" , originalTimestamp ))
611866
612- err = clientset .CoreV1 ().Secrets (namespace_certman_operator ).Delete (ctx , secretNameToDelete , metav1.DeleteOptions {})
867+ err = clientset .CoreV1 ().Secrets (operatorNS ).Delete (ctx , secretNameToDelete , metav1.DeleteOptions {})
613868 gomega .Expect (err ).ShouldNot (gomega .HaveOccurred (), "Failed to delete the secret" )
614869
615870 gomega .Eventually (func () bool {
616- newSecret , err := clientset .CoreV1 ().Secrets (namespace_certman_operator ).Get (ctx , secretNameToDelete , metav1.GetOptions {})
871+ newSecret , err := clientset .CoreV1 ().Secrets (operatorNS ).Get (ctx , secretNameToDelete , metav1.GetOptions {})
617872 if err != nil {
618873 return false
619874 }
0 commit comments