// Package config provides configuration management for the home-ip-updater service. // It handles environment variable validation and provides a centralized configuration // structure for AWS and RabbitMQ settings. package config import ( "cmp" "errors" "os" rabbitmqconfig "github.com/a-castellano/go-types/rabbitmq" ) // Config contains all configuration variables required by the home-ip-updater service. // It includes settings for AWS Route53, RabbitMQ, and domain configuration. type Config struct { AWSZoneID string // AWS Route53 hosted zone ID for DNS updates Subdomain string // Subdomain to update with new IP addresses UpdateQueue string // RabbitMQ queue name for receiving IP updates RabbitmqConfig *rabbitmqconfig.Config // RabbitMQ connection configuration } // NewConfig validates and loads all required environment variables into a Config struct. // It performs validation for AWS credentials and RabbitMQ configuration. // Returns an error if any required environment variables are missing or invalid. // // Required environment variables: // - AWS_ACCESS_KEY_ID: AWS access key for Route53 API access // - AWS_SECRET_ACCESS_KEY: AWS secret key for Route53 API access // - AWS_ZONE_ID: Route53 hosted zone ID // - SUBDOMAIN: Subdomain to update // // Optional environment variables: // - UPDATE_QUEUE_NAME: RabbitMQ queue name (defaults to "home-ip-monitor-updates") // - AWS_REGION: AWS region (defaults to "us-west-2") func NewConfig() (*Config, error) { config := Config{} var envVariableFound bool // Validate AWS credentials - required for Route53 DNS updates if _, envVariableFound = os.LookupEnv("AWS_ACCESS_KEY_ID"); !envVariableFound { return nil, errors.New("AWS_ACCESS_KEY_ID env variable must be set") } if _, envVariableFound = os.LookupEnv("AWS_SECRET_ACCESS_KEY"); !envVariableFound { return nil, errors.New("AWS_SECRET_ACCESS_KEY env variable must be set") } // Set RabbitMQ queue name with default value config.UpdateQueue = cmp.Or(os.Getenv("UPDATE_QUEUE_NAME"), "home-ip-monitor-updates") // Set AWS region with default value AWSRegion := cmp.Or(os.Getenv("AWS_REGION"), "us-west-2") os.Setenv("AWS_REGION", AWSRegion) // Validate AWS Route53 configuration if config.AWSZoneID, envVariableFound = os.LookupEnv("AWS_ZONE_ID"); !envVariableFound { return nil, errors.New("AWS_ZONE_ID env variable must be set") } if config.Subdomain, envVariableFound = os.LookupEnv("SUBDOMAIN"); !envVariableFound { return nil, errors.New("SUBDOMAIN env variable must be set") } // Load RabbitMQ configuration from environment variables var rabbitmqConfigErr error config.RabbitmqConfig, rabbitmqConfigErr = rabbitmqconfig.NewConfig() if rabbitmqConfigErr != nil { return nil, rabbitmqConfigErr } return &config, nil }
// Package main provides the home-ip-updater service that monitors a RabbitMQ queue // for IP address updates and automatically updates DNS records in AWS Route53. // This service is designed to work with the home-ip-monitor system to keep // a subdomain pointing to the current home IP address. package main import ( "context" "log" "log/syslog" "os" "os/signal" "syscall" messagebroker "github.com/a-castellano/go-services/messagebroker" config "github.com/a-castellano/home-ip-updater/config" updater "github.com/a-castellano/home-ip-updater/updater" ) // main is the entry point of the home-ip-updater service. // It sets up logging, configuration, message broker connection, // signal handling, and starts the main message processing loop. func main() { // Configure logger to write to syslog for system integration // This allows the service to integrate with system logging infrastructure logwriter, e := syslog.New(syslog.LOG_INFO, "home-ip-updater") if e == nil { log.SetOutput(logwriter) // Remove timestamp from log messages as syslog already provides timestamps log.SetFlags(0) } log.Print("Loading configuration from environment variables") // Load application configuration from environment variables // This validates all required AWS, RabbitMQ, and domain settings appConfig, configErr := config.NewConfig() if configErr != nil { log.Print(configErr.Error()) os.Exit(1) } log.Print("Creating RabbitMQ client for message consumption") // Create a cancellable context for graceful shutdown ctx, cancel := context.WithCancel(context.Background()) // Initialize RabbitMQ client and message broker rabbitmqClient := messagebroker.NewRabbitmqClient(appConfig.RabbitmqConfig) messageBroker := messagebroker.MessageBroker{Client: rabbitmqClient} // Create channels for message processing and error handling messagesReceived := make(chan []byte) receiveErrors := make(chan error) log.Print("Setting up OS signal handling for graceful shutdown") // Set up signal handling for graceful shutdown (SIGTERM, SIGINT) signalChannel := make(chan os.Signal, 2) signal.Notify(signalChannel, os.Interrupt, syscall.SIGTERM) // Start signal handler goroutine go func() { sig := <-signalChannel switch sig { case os.Interrupt: log.Print("Received SIGINT, initiating graceful shutdown") cancel() case syscall.SIGTERM: log.Print("Received SIGTERM, initiating graceful shutdown") cancel() } }() // Start message consumer in background go messageBroker.ReceiveMessages(ctx, appConfig.UpdateQueue, messagesReceived, receiveErrors) log.Print("Starting main message processing loop") // Main processing loop - handles messages and errors for { select { case receivedError := <-receiveErrors: // Handle RabbitMQ connection or message processing errors log.Print(receivedError.Error()) os.Exit(1) case messageReceived := <-messagesReceived: // Process received IP address update ipReceived := string(messageReceived) log.Printf("Received new IP address to update: %s", ipReceived) log.Printf("Updating DNS record for subdomain: %s", appConfig.Subdomain) // Create AWS updater instance with current configuration awsUpdater := updater.AWSUpdater{ ZoneID: appConfig.AWSZoneID, Subdomain: appConfig.Subdomain, IP: ipReceived, } // Update DNS record in AWS Route53 updateErr := awsUpdater.Update(ctx) if updateErr != nil { log.Printf("Failed to update DNS record: %s", updateErr.Error()) } else { log.Printf("Successfully updated DNS record for %s to IP %s", appConfig.Subdomain, ipReceived) } case <-ctx.Done(): // Handle graceful shutdown log.Print("Shutdown signal received, terminating service") os.Exit(0) } } }
// Package updater provides DNS record update functionality for the home-ip-updater service. // It includes implementations for updating DNS records in AWS Route53 and other DNS providers. package updater import ( "context" aws "github.com/aws/aws-sdk-go-v2/aws" awsconfig "github.com/aws/aws-sdk-go-v2/config" route53 "github.com/aws/aws-sdk-go-v2/service/route53" r53types "github.com/aws/aws-sdk-go-v2/service/route53/types" ) // Updater defines an interface for DNS record updates to allow for easy testing // and potential support for multiple DNS providers. This interface can be implemented // for different DNS services like AWS Route53, Cloudflare, etc. type Updater interface { // Update performs the DNS record update operation. // It should update the specified DNS record with the new IP address. // Returns an error if the update operation fails. Update(context.Context) error } // AWSUpdater implements the Updater interface for AWS Route53 DNS service. // It handles updating A records in a specified Route53 hosted zone. type AWSUpdater struct { ZoneID string // AWS Route53 hosted zone ID Subdomain string // Subdomain to update (e.g., "home.example.com") IP string // New IP address to set for the subdomain } // Update performs a DNS record update in AWS Route53. // It creates or updates an A record for the specified subdomain with the new IP address. // The method uses AWS SDK v2 and requires proper AWS credentials to be configured. // // The update operation: // - Uses UPSERT action to create or update the A record // - Sets TTL to 60 seconds for quick propagation // - Adds a comment for tracking purposes // - Handles AWS API errors and returns them as Go errors // // Returns an error if: // - AWS credentials are invalid or missing // - The hosted zone ID is invalid // - The Route53 API call fails // - The subdomain format is invalid func (awsupdater *AWSUpdater) Update(ctx context.Context) error { // Load AWS configuration from environment variables or IAM roles awscfg, err := awsconfig.LoadDefaultConfig(ctx) if err != nil { return err } // Create Route53 client with loaded configuration client := route53.NewFromConfig(awscfg) // Prepare the DNS record change request input := &route53.ChangeResourceRecordSetsInput{ ChangeBatch: &r53types.ChangeBatch{ Changes: []r53types.Change{ { Action: r53types.ChangeActionUpsert, // Create or update the record ResourceRecordSet: &r53types.ResourceRecordSet{ Name: aws.String(awsupdater.Subdomain), // Subdomain to update Type: r53types.RRTypeA, // A record type for IPv4 addresses TTL: aws.Int64(60), // 60 second TTL for quick updates ResourceRecords: []r53types.ResourceRecord{ { Value: aws.String(awsupdater.IP), // New IP address }, }, }, }, }, Comment: aws.String("Updated by home-ip-updater"), // Tracking comment }, HostedZoneId: aws.String(awsupdater.ZoneID), // Target hosted zone } // Execute the DNS record change _, errChange := client.ChangeResourceRecordSets(ctx, input) if errChange != nil { return errChange } return nil }