Skip to content

Commit e6986ec

Browse files
committed
Added E2E testcase for ACME DNS-01 challenge
1 parent 59f10dc commit e6986ec

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) {
@@ -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

Comments
 (0)