Add r53 command

This commit is contained in:
dwrz
2026-10-24 02:44:18 +00:00
parent 5533ade854
commit c32eb923b8
+175
View File
@@ -0,0 +1,175 @@
package main
import (
"context"
"encoding/json/jsontext"
"encoding/json/v2"
"flag"
"fmt"
"log/slog"
"os"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/route53"
"github.com/aws/aws-sdk-go-v2/service/route53/types"
"code.chimeric.al/dwrz/r53/pkg/digip"
)
var (
ttl = flag.Int64("ttl", 300, "TTL of the DNS record; seconds")
zone = flag.String("zone", "", "hosted zone id")
)
func main() {
flag.Parse()
var log = slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
AddSource: true,
Level: slog.LevelDebug,
}))
// Setup the main context.
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
if err := run(ctx, log); err != nil {
log.Error("failed", slog.Any("error", err))
os.Exit(1)
}
}
func run(ctx context.Context, log *slog.Logger) error {
if *zone == "" {
return fmt.Errorf("missing hosted zone id")
}
// Load the AWS configuration.
awscfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
return fmt.Errorf("failed to retrieve AWS config: %w", err)
}
// Create the Route53 client.
var svc = route53.NewFromConfig(awscfg)
// Get the IP address.
var ip4, ip6 string
// Dig with OpenDNS.
if ip, err := digip.OpenDNS(ctx, digip.A); err != nil {
log.ErrorContext(
ctx,
"failed to get opendns ipv4 ip",
slog.Any("error", err),
)
} else {
ip4 = ip.String()
}
if ip, err := digip.OpenDNS(ctx, digip.AAAA); err != nil {
log.ErrorContext(
ctx,
"failed to get opendns ipv6 ip",
slog.Any("error", err),
)
} else {
ip6 = ip.String()
}
// Fallback on Google.
if ip4 == "" {
if ip, err := digip.Google(ctx, digip.V4); err != nil {
log.ErrorContext(
ctx,
"failed to get google ipv4 ip",
slog.Any("error", err),
)
} else {
ip4 = ip.String()
}
}
if ip6 == "" {
if ip, err := digip.Google(ctx, digip.V6); err != nil {
log.ErrorContext(
ctx,
"failed to get google ipv6 ip",
slog.Any("error", err),
)
} else {
ip6 = ip.String()
}
}
// Abort if we don't have any IP address.
if ip4 == "" && ip6 == "" {
return fmt.Errorf("failed to retrieve public ip")
}
// Update the domains.
for _, domain := range flag.Args() {
// Assemble the change(s).
var changes []types.Change
if ip4 != "" {
change := types.Change{
Action: types.ChangeActionUpsert,
ResourceRecordSet: &types.ResourceRecordSet{
Name: &domain,
ResourceRecords: []types.ResourceRecord{
{Value: &ip4},
},
TTL: ttl,
Type: types.RRTypeA,
},
}
changes = append(changes, change)
}
if ip6 != "" {
change := types.Change{
Action: types.ChangeActionUpsert,
ResourceRecordSet: &types.ResourceRecordSet{
Name: &domain,
ResourceRecords: []types.ResourceRecord{
{Value: &ip6}},
TTL: ttl,
Type: types.RRTypeAaaa,
},
}
changes = append(changes, change)
}
input := &route53.ChangeResourceRecordSetsInput{
ChangeBatch: &types.ChangeBatch{
Changes: changes,
Comment: aws.String("DDNS"),
},
HostedZoneId: zone,
}
result, err := svc.ChangeResourceRecordSets(ctx, input)
if err != nil {
return fmt.Errorf("failed to update route 53: %w", err)
}
outcome, err := json.Marshal(result, jsontext.WithIndent(" "))
if err != nil {
log.ErrorContext(
ctx,
"failed to unmarshal response",
slog.Any("error", err),
)
outcome = []byte(result.ChangeInfo.Status)
}
log.DebugContext(
ctx,
"updated route 53 records",
slog.String("domain", domain),
slog.Any("outcome", outcome),
)
}
return nil
}