main
go 710 lines 20.2 KB
Raw
1 package route53
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "strconv"
8 "strings"
9 "time"
10
11 "github.com/aws/aws-sdk-go-v2/aws"
12 "github.com/aws/aws-sdk-go-v2/config"
13 "github.com/aws/aws-sdk-go-v2/credentials"
14 awsroute53 "github.com/aws/aws-sdk-go-v2/service/route53"
15 route53types "github.com/aws/aws-sdk-go-v2/service/route53/types"
16 "github.com/go-acme/lego/v4/challenge"
17 "github.com/go-acme/lego/v4/providers/dns/route53"
18
19 "github.com/gosuda/portal-tunnel/v2/utils"
20 )
21
22 const (
23 defaultAWSRegion = "us-east-1"
24 defaultDNSSECKSKName = "portal_ksk"
25 )
26
27 type Config struct {
28 AccessKeyID string
29 SecretAccessKey string
30 SessionToken string
31 Region string
32 HostedZoneID string
33 KMSKeyARN string
34 }
35
36 type Provider struct {
37 cfg Config
38 zones *utils.Snapshot[map[string]string]
39 }
40
41 func New(cfg Config) *Provider {
42 return &Provider{
43 cfg: Config{
44 AccessKeyID: strings.TrimSpace(cfg.AccessKeyID),
45 SecretAccessKey: strings.TrimSpace(cfg.SecretAccessKey),
46 SessionToken: strings.TrimSpace(cfg.SessionToken),
47 Region: strings.TrimSpace(cfg.Region),
48 HostedZoneID: normalizeZoneID(cfg.HostedZoneID),
49 KMSKeyARN: strings.TrimSpace(cfg.KMSKeyARN),
50 },
51 zones: utils.NewSnapshot(map[string]string{}, utils.CloneMap[string, string]),
52 }
53 }
54
55 func (p *Provider) Name() string {
56 return "route53"
57 }
58
59 func (p *Provider) ChallengeProvider(context.Context) (challenge.Provider, error) {
60 if p == nil {
61 return nil, errors.New("route53 provider is nil")
62 }
63 if err := validateConfig(p.cfg); err != nil {
64 return nil, err
65 }
66
67 cfg := route53.NewDefaultConfig()
68 cfg.AccessKeyID = p.cfg.AccessKeyID
69 cfg.SecretAccessKey = p.cfg.SecretAccessKey
70 cfg.SessionToken = p.cfg.SessionToken
71 cfg.Region = p.awsRegion()
72 cfg.HostedZoneID = p.cfg.HostedZoneID
73
74 provider, err := route53.NewDNSProviderConfig(cfg)
75 if err != nil {
76 return nil, fmt.Errorf("create route53 lego provider: %w", err)
77 }
78 return provider, nil
79 }
80
81 func (p *Provider) EnsureARecords(ctx context.Context, baseDomain, publicIPv4 string) error {
82 if p == nil {
83 return errors.New("route53 provider is nil")
84 }
85 baseDomain = utils.NormalizeBaseDomain(baseDomain)
86 if baseDomain == "" {
87 return errors.New("base domain is required")
88 }
89 if err := utils.ValidateIPv4(publicIPv4); err != nil {
90 return err
91 }
92
93 client, err := newClient(ctx, p.cfg)
94 if err != nil {
95 return err
96 }
97
98 hostedZoneID, err := p.findHostedZoneID(ctx, client, baseDomain)
99 if err != nil {
100 return err
101 }
102
103 for _, recordName := range []string{baseDomain, "*." + baseDomain} {
104 if err := upsertARecord(ctx, client, hostedZoneID, recordName, publicIPv4); err != nil {
105 return fmt.Errorf("upsert route53 A record %s: %w", recordName, err)
106 }
107 }
108 return nil
109 }
110
111 func (p *Provider) EnsureARecord(ctx context.Context, name, publicIPv4 string) error {
112 if p == nil {
113 return errors.New("route53 provider is nil")
114 }
115 name = utils.NormalizeHostname(name)
116 if name == "" {
117 return errors.New("record name is required")
118 }
119 if err := utils.ValidateIPv4(publicIPv4); err != nil {
120 return err
121 }
122
123 client, err := newClient(ctx, p.cfg)
124 if err != nil {
125 return err
126 }
127
128 hostedZoneID, err := p.findHostedZoneID(ctx, client, name)
129 if err != nil {
130 return err
131 }
132 if err := upsertARecord(ctx, client, hostedZoneID, name, publicIPv4); err != nil {
133 return fmt.Errorf("upsert route53 A record %s: %w", name, err)
134 }
135 return nil
136 }
137
138 func (p *Provider) DeleteARecord(ctx context.Context, name string) error {
139 if p == nil {
140 return errors.New("route53 provider is nil")
141 }
142 name = utils.NormalizeHostname(name)
143 if name == "" {
144 return errors.New("record name is required")
145 }
146
147 client, err := newClient(ctx, p.cfg)
148 if err != nil {
149 return err
150 }
151
152 hostedZoneID, err := p.findHostedZoneID(ctx, client, name)
153 if err != nil {
154 return err
155 }
156 recordSet, err := getRecordSet(ctx, client, hostedZoneID, name, route53types.RRTypeA)
157 if err != nil {
158 return err
159 }
160 if recordSet == nil {
161 return nil
162 }
163 if err := deleteRecordSet(ctx, client, hostedZoneID, recordSet, "Managed by Portal ENS cleanup"); err != nil {
164 return fmt.Errorf("delete route53 A record %s: %w", name, err)
165 }
166 return nil
167 }
168
169 func (p *Provider) EnsureTXTRecord(ctx context.Context, name, value string) error {
170 if p == nil {
171 return errors.New("route53 provider is nil")
172 }
173 name = utils.NormalizeHostname(name)
174 if name == "" {
175 return errors.New("record name is required")
176 }
177 value = strings.TrimSpace(value)
178 if value == "" {
179 return errors.New("txt record value is required")
180 }
181
182 client, err := newClient(ctx, p.cfg)
183 if err != nil {
184 return err
185 }
186
187 hostedZoneID, err := p.findHostedZoneID(ctx, client, name)
188 if err != nil {
189 return err
190 }
191 if err := ensureTXTRecord(ctx, client, hostedZoneID, name, value); err != nil {
192 return fmt.Errorf("upsert route53 TXT record %s: %w", name, err)
193 }
194 return nil
195 }
196
197 func (p *Provider) DeleteTXTRecords(ctx context.Context, name, matchPrefix string) error {
198 if p == nil {
199 return errors.New("route53 provider is nil")
200 }
201 name = utils.NormalizeHostname(name)
202 if name == "" {
203 return errors.New("record name is required")
204 }
205 matchPrefix = strings.TrimSpace(matchPrefix)
206 if matchPrefix == "" {
207 return errors.New("txt record match prefix is required")
208 }
209
210 client, err := newClient(ctx, p.cfg)
211 if err != nil {
212 return err
213 }
214
215 hostedZoneID, err := p.findHostedZoneID(ctx, client, name)
216 if err != nil {
217 return err
218 }
219 if err := deleteTXTRecords(ctx, client, hostedZoneID, name, matchPrefix); err != nil {
220 return fmt.Errorf("delete route53 TXT records %s: %w", name, err)
221 }
222 return nil
223 }
224
225 func (p *Provider) EnsureHTTPSRecord(ctx context.Context, name string, _ uint16, _, _, content string) error {
226 if p == nil {
227 return errors.New("route53 provider is nil")
228 }
229 name = utils.NormalizeHostname(name)
230 if name == "" {
231 return errors.New("record name is required")
232 }
233 content = strings.TrimSpace(content)
234 if content == "" {
235 return errors.New("https record content is required")
236 }
237
238 client, err := newClient(ctx, p.cfg)
239 if err != nil {
240 return err
241 }
242
243 hostedZoneID, err := p.findHostedZoneID(ctx, client, name)
244 if err != nil {
245 return err
246 }
247 if err := upsertRecord(ctx, client, hostedZoneID, name, route53types.RRTypeHttps, []string{content}, "Managed by Portal ECH"); err != nil {
248 return fmt.Errorf("upsert route53 HTTPS record %s: %w", name, err)
249 }
250 return nil
251 }
252
253 func (p *Provider) DeleteHTTPSRecord(ctx context.Context, name string) error {
254 if p == nil {
255 return errors.New("route53 provider is nil")
256 }
257 name = utils.NormalizeHostname(name)
258 if name == "" {
259 return errors.New("record name is required")
260 }
261
262 client, err := newClient(ctx, p.cfg)
263 if err != nil {
264 return err
265 }
266
267 hostedZoneID, err := p.findHostedZoneID(ctx, client, name)
268 if err != nil {
269 return err
270 }
271 recordSet, err := getRecordSet(ctx, client, hostedZoneID, name, route53types.RRTypeHttps)
272 if err != nil {
273 return err
274 }
275 if recordSet == nil {
276 return nil
277 }
278 if err := deleteRecordSet(ctx, client, hostedZoneID, recordSet, "Managed by Portal ECH cleanup"); err != nil {
279 return fmt.Errorf("delete route53 HTTPS record %s: %w", name, err)
280 }
281 return nil
282 }
283
284 func (p *Provider) EnsureDNSSEC(ctx context.Context, baseDomain string) (state, dsRecord, message string, err error) {
285 if p == nil {
286 return "", "", "", errors.New("route53 provider is nil")
287 }
288 baseDomain = utils.NormalizeBaseDomain(baseDomain)
289 if baseDomain == "" {
290 return "", "", "", errors.New("base domain is required")
291 }
292
293 client, err := newClient(ctx, p.cfg)
294 if err != nil {
295 return "", "", "", err
296 }
297
298 hostedZoneID, err := p.findHostedZoneID(ctx, client, baseDomain)
299 if err != nil {
300 return "", "", "", err
301 }
302
303 out, err := getDNSSECStatus(ctx, client, hostedZoneID)
304 if err != nil {
305 return "", "", "", fmt.Errorf("get route53 dnssec status: %w", err)
306 }
307 state, dsRecord, message = dnssecStatusFromOutput(out)
308 if strings.EqualFold(state, "SIGNING") {
309 return state, dsRecord, message, nil
310 }
311
312 if _, ok := activeKeySigningKey(out.KeySigningKeys); !ok {
313 if err := ensureActiveKeySigningKey(ctx, client, hostedZoneID, p.cfg, out.KeySigningKeys); err != nil {
314 return "", "", "", err
315 }
316 out, err = getDNSSECStatus(ctx, client, hostedZoneID)
317 if err != nil {
318 return "", "", "", fmt.Errorf("refresh route53 dnssec status: %w", err)
319 }
320 if _, ok := activeKeySigningKey(out.KeySigningKeys); !ok {
321 return "", "", "", errors.New("route53 dnssec requires an ACTIVE key-signing key")
322 }
323 }
324
325 if _, err := client.EnableHostedZoneDNSSEC(ctx, &awsroute53.EnableHostedZoneDNSSECInput{
326 HostedZoneId: aws.String(hostedZoneID),
327 }); err != nil {
328 return "", "", "", fmt.Errorf("enable route53 dnssec: %w", err)
329 }
330
331 out, err = getDNSSECStatus(ctx, client, hostedZoneID)
332 if err != nil {
333 return "", "", "", fmt.Errorf("refresh route53 dnssec status: %w", err)
334 }
335 state, dsRecord, message = dnssecStatusFromOutput(out)
336 return state, dsRecord, message, nil
337 }
338
339 func newClient(ctx context.Context, cfg Config) (*awsroute53.Client, error) {
340 if err := validateConfig(cfg); err != nil {
341 return nil, err
342 }
343
344 loadOptions := []func(*config.LoadOptions) error{
345 config.WithRegion(regionOrDefault(cfg.Region)),
346 }
347 if cfg.AccessKeyID != "" && cfg.SecretAccessKey != "" {
348 loadOptions = append(loadOptions, config.WithCredentialsProvider(
349 credentials.NewStaticCredentialsProvider(cfg.AccessKeyID, cfg.SecretAccessKey, cfg.SessionToken),
350 ))
351 }
352
353 awsCfg, err := config.LoadDefaultConfig(ctx, loadOptions...)
354 if err != nil {
355 return nil, fmt.Errorf("load aws config: %w", err)
356 }
357 return awsroute53.NewFromConfig(awsCfg), nil
358 }
359
360 func (p *Provider) findHostedZoneID(ctx context.Context, client *awsroute53.Client, domain string) (string, error) {
361 if explicitZoneID := normalizeZoneID(p.cfg.HostedZoneID); explicitZoneID != "" {
362 return explicitZoneID, nil
363 }
364 if client == nil {
365 return "", errors.New("route53 client is nil")
366 }
367
368 candidates := utils.DomainCandidates(domain)
369 if len(candidates) == 0 {
370 return "", fmt.Errorf("invalid base domain for hosted zone lookup: %q", domain)
371 }
372
373 zones := p.zones.Load()
374 for _, candidate := range candidates {
375 if zoneID := zones[candidate]; zoneID != "" {
376 return zoneID, nil
377 }
378 }
379
380 zonesByName := make(map[string]string)
381 paginator := awsroute53.NewListHostedZonesPaginator(client, &awsroute53.ListHostedZonesInput{})
382 for paginator.HasMorePages() {
383 page, err := paginator.NextPage(ctx)
384 if err != nil {
385 return "", fmt.Errorf("list hosted zones: %w", err)
386 }
387 for _, hostedZone := range page.HostedZones {
388 if hostedZone.Config != nil && hostedZone.Config.PrivateZone {
389 continue
390 }
391 zoneName := utils.NormalizeHostname(aws.ToString(hostedZone.Name))
392 zoneID := normalizeZoneID(aws.ToString(hostedZone.Id))
393 if zoneName == "" || zoneID == "" {
394 continue
395 }
396 zonesByName[zoneName] = zoneID
397 }
398 }
399
400 if len(zonesByName) > 0 {
401 p.zones.UpdateCopy(func(zones *map[string]string) {
402 if *zones == nil {
403 *zones = make(map[string]string)
404 }
405 for zoneName, zoneID := range zonesByName {
406 (*zones)[zoneName] = zoneID
407 }
408 })
409 }
410
411 for _, candidate := range candidates {
412 if zoneID, ok := zonesByName[candidate]; ok {
413 return zoneID, nil
414 }
415 }
416
417 return "", fmt.Errorf("no route53 public hosted zone found for %s", domain)
418 }
419
420 func upsertARecord(ctx context.Context, client *awsroute53.Client, hostedZoneID, name, ip string) error {
421 return upsertRecord(ctx, client, hostedZoneID, name, route53types.RRTypeA, []string{strings.TrimSpace(ip)}, "Managed by Portal ACME")
422 }
423
424 func upsertTXTRecord(ctx context.Context, client *awsroute53.Client, hostedZoneID, name, value string) error {
425 return upsertRecord(ctx, client, hostedZoneID, name, route53types.RRTypeTxt, []string{route53TXTValue(value)}, "Managed by Portal ENS")
426 }
427
428 func ensureTXTRecord(ctx context.Context, client *awsroute53.Client, hostedZoneID, name, value string) error {
429 recordSet, err := getTXTRecordSet(ctx, client, hostedZoneID, name)
430 if err != nil {
431 return err
432 }
433 if recordSet == nil {
434 return upsertTXTRecord(ctx, client, hostedZoneID, name, value)
435 }
436
437 for _, record := range recordSet.ResourceRecords {
438 if route53TXTContent(aws.ToString(record.Value)) == value {
439 return nil
440 }
441 }
442
443 values := make([]string, 0, len(recordSet.ResourceRecords)+1)
444 for _, record := range recordSet.ResourceRecords {
445 values = append(values, aws.ToString(record.Value))
446 }
447 values = append(values, route53TXTValue(value))
448 return upsertRecord(ctx, client, hostedZoneID, name, route53types.RRTypeTxt, values, "Managed by Portal ENS")
449 }
450
451 func deleteTXTRecords(ctx context.Context, client *awsroute53.Client, hostedZoneID, name, matchPrefix string) error {
452 recordSet, err := getTXTRecordSet(ctx, client, hostedZoneID, name)
453 if err != nil {
454 return err
455 }
456 if recordSet == nil {
457 return nil
458 }
459
460 remaining := make([]string, 0, len(recordSet.ResourceRecords))
461 removed := false
462 for _, record := range recordSet.ResourceRecords {
463 value := aws.ToString(record.Value)
464 if strings.HasPrefix(route53TXTContent(value), matchPrefix) {
465 removed = true
466 continue
467 }
468 remaining = append(remaining, value)
469 }
470 if !removed {
471 return nil
472 }
473 if len(remaining) == 0 {
474 return deleteRecordSet(ctx, client, hostedZoneID, recordSet, "Managed by Portal ENS cleanup")
475 }
476 return upsertRecord(ctx, client, hostedZoneID, name, route53types.RRTypeTxt, remaining, "Managed by Portal ENS cleanup")
477 }
478
479 func upsertRecord(ctx context.Context, client *awsroute53.Client, hostedZoneID, name string, recordType route53types.RRType, values []string, comment string) error {
480 if client == nil {
481 return errors.New("route53 client is nil")
482 }
483 if hostedZoneID == "" {
484 return errors.New("hosted zone id is required")
485 }
486
487 fqdn := utils.NormalizeHostname(name)
488 if !strings.HasSuffix(fqdn, ".") {
489 fqdn += "."
490 }
491 recordSet := &route53types.ResourceRecordSet{
492 Name: aws.String(fqdn),
493 Type: recordType,
494 TTL: aws.Int64(60),
495 ResourceRecords: make([]route53types.ResourceRecord, 0, len(values)),
496 }
497 for _, value := range values {
498 recordSet.ResourceRecords = append(recordSet.ResourceRecords, route53types.ResourceRecord{
499 Value: aws.String(strings.TrimSpace(value)),
500 })
501 }
502
503 _, err := client.ChangeResourceRecordSets(ctx, &awsroute53.ChangeResourceRecordSetsInput{
504 HostedZoneId: aws.String(hostedZoneID),
505 ChangeBatch: &route53types.ChangeBatch{
506 Comment: aws.String(comment),
507 Changes: []route53types.Change{
508 {
509 Action: route53types.ChangeActionUpsert,
510 ResourceRecordSet: recordSet,
511 },
512 },
513 },
514 })
515 if err != nil {
516 return err
517 }
518 return nil
519 }
520
521 func route53TXTValue(value string) string {
522 return strconv.Quote(strings.TrimSpace(value))
523 }
524
525 func route53TXTContent(value string) string {
526 unquoted, err := strconv.Unquote(strings.TrimSpace(value))
527 if err == nil {
528 return unquoted
529 }
530 return strings.Trim(strings.TrimSpace(value), "\"")
531 }
532
533 func getTXTRecordSet(ctx context.Context, client *awsroute53.Client, hostedZoneID, name string) (*route53types.ResourceRecordSet, error) {
534 return getRecordSet(ctx, client, hostedZoneID, name, route53types.RRTypeTxt)
535 }
536
537 func getRecordSet(ctx context.Context, client *awsroute53.Client, hostedZoneID, name string, recordType route53types.RRType) (*route53types.ResourceRecordSet, error) {
538 if client == nil {
539 return nil, errors.New("route53 client is nil")
540 }
541 fqdn := utils.NormalizeHostname(name)
542 if !strings.HasSuffix(fqdn, ".") {
543 fqdn += "."
544 }
545
546 out, err := client.ListResourceRecordSets(ctx, &awsroute53.ListResourceRecordSetsInput{
547 HostedZoneId: aws.String(hostedZoneID),
548 StartRecordName: aws.String(fqdn),
549 StartRecordType: recordType,
550 MaxItems: aws.Int32(1),
551 })
552 if err != nil {
553 return nil, err
554 }
555 if len(out.ResourceRecordSets) == 0 {
556 return nil, nil
557 }
558 recordSet := out.ResourceRecordSets[0]
559 if !strings.EqualFold(strings.TrimSpace(aws.ToString(recordSet.Name)), fqdn) || recordSet.Type != recordType {
560 return nil, nil
561 }
562 return &recordSet, nil
563 }
564
565 func deleteRecordSet(ctx context.Context, client *awsroute53.Client, hostedZoneID string, recordSet *route53types.ResourceRecordSet, comment string) error {
566 if client == nil {
567 return errors.New("route53 client is nil")
568 }
569 if recordSet == nil {
570 return nil
571 }
572 _, err := client.ChangeResourceRecordSets(ctx, &awsroute53.ChangeResourceRecordSetsInput{
573 HostedZoneId: aws.String(hostedZoneID),
574 ChangeBatch: &route53types.ChangeBatch{
575 Comment: aws.String(comment),
576 Changes: []route53types.Change{
577 {
578 Action: route53types.ChangeActionDelete,
579 ResourceRecordSet: recordSet,
580 },
581 },
582 },
583 })
584 return err
585 }
586
587 func (p *Provider) awsRegion() string {
588 if p == nil {
589 return defaultAWSRegion
590 }
591 return regionOrDefault(p.cfg.Region)
592 }
593
594 func regionOrDefault(region string) string {
595 if trimmed := strings.TrimSpace(region); trimmed != "" {
596 return trimmed
597 }
598 return defaultAWSRegion
599 }
600
601 func validateConfig(cfg Config) error {
602 switch {
603 case cfg.SessionToken != "" && (cfg.AccessKeyID == "" || cfg.SecretAccessKey == ""):
604 return errors.New("route53 session token requires access key id and secret access key")
605 case (cfg.AccessKeyID == "") != (cfg.SecretAccessKey == ""):
606 return errors.New("route53 access key id and secret access key must be supplied together")
607 }
608 return nil
609 }
610
611 func normalizeZoneID(raw string) string {
612 trimmed := strings.TrimSpace(raw)
613 return strings.TrimPrefix(trimmed, "/hostedzone/")
614 }
615
616 func getDNSSECStatus(ctx context.Context, client *awsroute53.Client, hostedZoneID string) (*awsroute53.GetDNSSECOutput, error) {
617 if client == nil {
618 return nil, errors.New("route53 client is nil")
619 }
620 if hostedZoneID == "" {
621 return nil, errors.New("hosted zone id is required")
622 }
623 return client.GetDNSSEC(ctx, &awsroute53.GetDNSSECInput{
624 HostedZoneId: aws.String(hostedZoneID),
625 })
626 }
627
628 func ensureActiveKeySigningKey(ctx context.Context, client *awsroute53.Client, hostedZoneID string, cfg Config, keys []route53types.KeySigningKey) error {
629 if client == nil {
630 return errors.New("route53 client is nil")
631 }
632 kskName := defaultDNSSECKSKName
633
634 if existing, ok := keySigningKeyByName(keys, kskName); ok {
635 if strings.EqualFold(strings.TrimSpace(aws.ToString(existing.Status)), "ACTIVE") {
636 return nil
637 }
638 _, err := client.ActivateKeySigningKey(ctx, &awsroute53.ActivateKeySigningKeyInput{
639 HostedZoneId: aws.String(hostedZoneID),
640 Name: aws.String(kskName),
641 })
642 if err != nil {
643 return fmt.Errorf("activate route53 key-signing key %q: %w", kskName, err)
644 }
645 return nil
646 }
647
648 if strings.TrimSpace(cfg.KMSKeyARN) == "" {
649 return errors.New("route53 dnssec requires AWS_DNSSEC_KMS_KEY_ARN when no active key-signing key exists")
650 }
651
652 _, err := client.CreateKeySigningKey(ctx, &awsroute53.CreateKeySigningKeyInput{
653 CallerReference: aws.String(fmt.Sprintf("portal-%d", time.Now().UTC().UnixNano())),
654 HostedZoneId: aws.String(hostedZoneID),
655 KeyManagementServiceArn: aws.String(cfg.KMSKeyARN),
656 Name: aws.String(kskName),
657 Status: aws.String("ACTIVE"),
658 })
659 if err != nil {
660 var alreadyExists *route53types.KeySigningKeyAlreadyExists
661 if errors.As(err, &alreadyExists) {
662 return nil
663 }
664 return fmt.Errorf("create route53 key-signing key %q: %w", kskName, err)
665 }
666 return nil
667 }
668
669 func dnssecStatusFromOutput(out *awsroute53.GetDNSSECOutput) (state, dsRecord, message string) {
670 if out == nil {
671 return "", "", ""
672 }
673
674 if out.Status != nil {
675 state = strings.TrimSpace(aws.ToString(out.Status.ServeSignature))
676 message = strings.TrimSpace(aws.ToString(out.Status.StatusMessage))
677 }
678 if active, ok := activeKeySigningKey(out.KeySigningKeys); ok {
679 dsRecord = strings.TrimSpace(aws.ToString(active.DSRecord))
680 } else {
681 for _, key := range out.KeySigningKeys {
682 if strings.TrimSpace(aws.ToString(key.DSRecord)) != "" {
683 dsRecord = strings.TrimSpace(aws.ToString(key.DSRecord))
684 break
685 }
686 }
687 }
688 if message == "" && dsRecord != "" {
689 message = "publish the DS record at the registrar after Route53 zone signing is enabled"
690 }
691 return state, dsRecord, message
692 }
693
694 func activeKeySigningKey(keys []route53types.KeySigningKey) (route53types.KeySigningKey, bool) {
695 for _, key := range keys {
696 if strings.EqualFold(strings.TrimSpace(aws.ToString(key.Status)), "ACTIVE") {
697 return key, true
698 }
699 }
700 return route53types.KeySigningKey{}, false
701 }
702
703 func keySigningKeyByName(keys []route53types.KeySigningKey, name string) (route53types.KeySigningKey, bool) {
704 for _, key := range keys {
705 if strings.EqualFold(strings.TrimSpace(aws.ToString(key.Name)), name) {
706 return key, true
707 }
708 }
709 return route53types.KeySigningKey{}, false
710 }