diff --git a/pkg/awsclient/client.go b/pkg/awsclient/client.go index 81246094c..6b0a91ddb 100644 --- a/pkg/awsclient/client.go +++ b/pkg/awsclient/client.go @@ -36,6 +36,7 @@ import ( const ( awsCredsSecretIDKey = "aws_access_key_id" awsCredsSecretAccessKey = "aws_secret_access_key" + resourceRecordTTL = 60 ) // Client is a wrapper object for actual AWS SDK clients to allow for easier testing. @@ -78,6 +79,63 @@ func (c *awsClient) ListResourceRecordSets(input *route53.ListResourceRecordSets return c.route53Client.ListResourceRecordSets(input) } +// SearchForHostedZone finds a hostedzone when given an aws client and a domain string +// Returns a hostedzone object +func SearchForHostedZone(r53svc Client, baseDomain string) (hostedZone route53.HostedZone, err error) { + hostedZoneOutput, err := r53svc.ListHostedZones(&route53.ListHostedZonesInput{}) + if err != nil { + return hostedZone, err + } + + for _, zone := range hostedZoneOutput.HostedZones { + if strings.EqualFold(baseDomain, *zone.Name) && !*zone.Config.PrivateZone { + hostedZone = *zone + } + } + return hostedZone, err +} + +// BuildR53Input contructs an Input object for a hostedzone. Contains no recordsets. +func BuildR53Input(hostedZone string) *route53.ChangeResourceRecordSetsInput { + input := &route53.ChangeResourceRecordSetsInput{ + ChangeBatch: &route53.ChangeBatch{ + Changes: []*route53.Change{}, + }, + HostedZoneId: &hostedZone, + } + return input +} + +// CreateR53TXTRecordChange creates an route53 Change object for a TXT record with a given name +// and a given action to take. Valid actions are strings matching valid route53 ChangeActions. +func CreateR53TXTRecordChange(name *string, action string, value *string) (change route53.Change, err error) { + // Checking the string 'action' to see if it matches any of the valid route53 acctions. + // If an incorrect string value is passed this function will exit and raise an error. + if strings.EqualFold("DELETE", action) { + action = route53.ChangeActionDelete + } else if strings.EqualFold("CREATE", action) { + action = route53.ChangeActionCreate + } else if strings.EqualFold("UPSERT", action) { + action = route53.ChangeActionUpsert + } else { + return change, fmt.Errorf("Invaild record action passed %v. Must be DELETE, CREATE, or UPSERT", action) + } + change = route53.Change{ + Action: aws.String(action), + ResourceRecordSet: &route53.ResourceRecordSet{ + Name: aws.String(*name), + ResourceRecords: []*route53.ResourceRecord{ + { + Value: aws.String(*value), + }, + }, + TTL: aws.Int64(resourceRecordTTL), + Type: aws.String(route53.RRTypeTxt), + }, + } + return change, nil +} + // NewClient returns an awsclient.Client object to the caller. If NewClient is passed a non-null // secretName, an attempt to retrieve the secret from the namespace argument will be performed. // AWS credentials are returned as these secrets and a new session is initiated prior to returning diff --git a/pkg/controller/certificaterequest/dns.go b/pkg/controller/certificaterequest/dns.go index 10187f2c9..8ee7faaed 100644 --- a/pkg/controller/certificaterequest/dns.go +++ b/pkg/controller/certificaterequest/dns.go @@ -26,6 +26,7 @@ import ( "github.com/go-logr/logr" certmanv1alpha1 "github.com/openshift/certman-operator/pkg/apis/certman/v1alpha1" + "github.com/openshift/certman-operator/pkg/awsclient" ) // AnswerDnsChallenge constructs a fqdn from acmeChallengeSubDomain and domain. An route53 AWS client is then spawned to retrieve HostedZones. @@ -271,6 +272,64 @@ func (r *ReconcileCertificateRequest) DeleteAcmeChallengeResourceRecords(reqLogg return nil } +// DeleteAllAcmeChallengeResourceRecords to delete all records in a hosted zone that begin with the prefix defined by the const acmeChallengeSubDomain +func (r *ReconcileCertificateRequest) DeleteAllAcmeChallengeResourceRecords(reqLogger logr.Logger, cr *certmanv1alpha1.CertificateRequest) error { + // This function is for record clean up. If we are unable to find the records to delete them we silently accept these errors + // without raising an error. If the record was already deleted that's fine. + + r53svc, err := r.getAwsClient(cr) + if err != nil { + return err + } + + // Make sure that the domain ends with a dot. + baseDomain := cr.Spec.ACMEDNSDomain + if string(baseDomain[len(baseDomain)-1]) != "." { + baseDomain = baseDomain + "." + } + + // Calls function to get the hostedzone of the domain of our CertificateRequest + hostedzone, err := awsclient.SearchForHostedZone(r53svc, baseDomain) + if err != nil { + reqLogger.Error(err, "Unable to find appropriate hostedzone.") + return err + } + + // Get a list of RecordSets from our hostedzone that match our search criteria + // Criteria - record name starts with our acmechallenge prefix, record is a TXT type + listRecordSets, err := r53svc.ListResourceRecordSets(&route53.ListResourceRecordSetsInput{ + HostedZoneId: aws.String(*hostedzone.Id), // Required + StartRecordName: aws.String(acmeChallengeSubDomain + "*"), + StartRecordType: aws.String(route53.RRTypeTxt), + }) + if err != nil { + reqLogger.Error(err, "Unable to retrieve acme records for hostedzone.") + return err + } + + // Construct an Input object and populate it with records we intend to change + // In this case we're adding all acme challenge records found above and setting their action to Delete + input := awsclient.BuildR53Input(*hostedzone.Id) + for _, record := range listRecordSets.ResourceRecordSets { + if strings.Contains(*record.Name, acmeChallengeSubDomain) { + change, err := awsclient.CreateR53TXTRecordChange(record.Name, route53.ChangeActionDelete, record.ResourceRecords[0].Value) + if err != nil { + reqLogger.Error(err, "Error creating record change object") + } + input.ChangeBatch.Changes = append(input.ChangeBatch.Changes, &change) + } + } + + // Sent the completed Input object to Route53 to delete the acme records + result, err := r53svc.ChangeResourceRecordSets(input) + if err != nil { + reqLogger.Error(err, result.GoString()) + return nil + } + + return nil +} + // func newTXTRecordSet(fqdn, value string, ttl int) *route53.ResourceRecordSet { // return &route53.ResourceRecordSet{ // Name: aws.String(fqdn), diff --git a/pkg/controller/certificaterequest/issue_certificate.go b/pkg/controller/certificaterequest/issue_certificate.go index 61c81fbb9..72ce2ab57 100644 --- a/pkg/controller/certificaterequest/issue_certificate.go +++ b/pkg/controller/certificaterequest/issue_certificate.go @@ -197,7 +197,7 @@ func (r *ReconcileCertificateRequest) IssueCertificate(reqLogger logr.Logger, cr // After resolving all new challenges, and storing the cert, delete the challenge records // that were used from dns in this zone. - err = r.DeleteAcmeChallengeResourceRecords(reqLogger, cr) + err = r.DeleteAllAcmeChallengeResourceRecords(reqLogger, cr) if err != nil { reqLogger.Error(err, "error occurred deleting acme challenge resource records from Route53") }