diff --git a/cmd/woodpecker/main.go b/cmd/woodpecker/main.go index 308a99e..d22a126 100644 --- a/cmd/woodpecker/main.go +++ b/cmd/woodpecker/main.go @@ -2,31 +2,35 @@ package main import ( "fmt" - "github.com/robfig/cron/v3" "os" + "woodpecker/internal/config" "woodpecker/internal/providers/namecheap" "woodpecker/internal/providers/porkbun" "woodpecker/internal/services" "woodpecker/internal/utils" + + "github.com/robfig/cron/v3" ) func main() { + utils.InitLogger() + configDir, err := utils.GetAppPath() if err != nil { - fmt.Println("failed to get config path:", err) + utils.Log.Error().Err(err).Msg("an error occurred") os.Exit(1) } loadConfig, err := config.LoadConfig() if err != nil { - fmt.Println("error loading environment file:", err) + utils.Log.Error().Err(err).Msg("an error occurred") os.Exit(1) } err = updateDNS(loadConfig, configDir) if err != nil { - fmt.Printf("error during initial DNS update: %v\n", err) + utils.Log.Error().Err(err).Msg("an error occurred") os.Exit(1) } @@ -42,12 +46,12 @@ func setupCron(config *config.Config, configPath string) { _, err := c.AddFunc(checkInterval, func() { err := updateDNS(config, configPath) if err != nil { - fmt.Printf("error during DNS update: %v\n", err) + utils.Log.Error().Err(err).Msg("error during DNS update: " + err.Error()) } }) if err != nil { - fmt.Println("failed to schedule DNS update:", err) + utils.Log.Fatal().Err(err).Msg("failed to schedule DNS update") os.Exit(1) } @@ -57,30 +61,26 @@ func setupCron(config *config.Config, configPath string) { func updateDNS(config *config.Config, configPath string) error { ip, err := services.GetPublicIP(config) if err != nil { - return fmt.Errorf("failed to retrieve public IP: %v", err) + return err } - fmt.Println("current public IP address:", ip) storedIP, err := utils.ReadIPFromFile(configPath) if err != nil { - return fmt.Errorf("failed to read stored IP: %v", err) + return err } - fmt.Println("current stored public IP address:", storedIP) if storedIP == ip { - fmt.Println("IP address unchanged, skipping DNS updates") + utils.Log.Info().Msgf("public and stored IP address equal (%s), sleeping for %d minute(s)", ip, config.CheckInterval) return nil } - fmt.Println("IP address has changed, proceeding to update DNS records...") + utils.Log.Info().Msg("public and stored IP address not equal, proceeding to update DNS records...") if config.PorkbunAPIKey != "" && config.PorkbunSecretKey != "" { err = updatePorkbunDNS(config, ip) if err != nil { return err } - } else { - fmt.Println("skipping Porkbun DNS update as required config not provided") } if config.NamecheapPassword != "" { @@ -88,52 +88,51 @@ func updateDNS(config *config.Config, configPath string) error { if err != nil { return err } - } else { - fmt.Println("skipping Namecheap DNS update as required config not provided") } err = utils.WriteIPToFile(ip, configPath) if err != nil { - return fmt.Errorf("failed to store new updated public IP address: %v", err) + return err } - fmt.Println("DNS updates completed for both providers.") + utils.Log.Info().Str("level", "update").Msgf("update complete, sleeping for %d minute(s)", config.CheckInterval) return nil } func updatePorkbunDNS(config *config.Config, ip string) error { - fmt.Println("checking Porkbun DNS records...") + utils.Log.Info().Msg("checking Porkbun DNS records...") porkbunProvider := porkbun.New(config) dnsIP, err := porkbunProvider.GetCurrentARecord() if err != nil { - return fmt.Errorf("failed to retrieve Porkbun DNS A record: %v", err) + utils.Log.Error().Err(err).Msg("failed to retrieve Porkbun DNS A record") + return err } - fmt.Println("current Porkbun DNS A record IP address:", dnsIP) + utils.Log.Info().Msg("current Porkbun DNS A record IP address") if dnsIP != ip { - fmt.Printf("Porkbun DNS record is outdated- current: %s, expected: %s\n", dnsIP, ip) + utils.Log.Info().Msg("Porkbun DNS record is outdated") err := porkbunProvider.UpdateARecord(ip) if err != nil { - return fmt.Errorf("failed to update Porkbun DNS A record: %v", err) + return fmt.Errorf("failed to update Porkbun DNS A record: %w", err) } - fmt.Println("Porkbun DNS A record updated successfully.") - } else { - fmt.Println("Porkbun DNS A record already up to date.") + + utils.Log.Info().Str("level", "update").Msgf("PorkBun (%s.%s) | DNS A record updated successfully.", config.PorkbunSubdomain, config.PorkbunDomain) } return nil } func updateNamecheapDNS(config *config.Config, ip string) error { - fmt.Println("updating Namecheap DNS records...") + utils.Log.Info().Str("level", "update").Msg("updating Namecheap DNS records...") namecheapProvider := namecheap.New(config) err := namecheapProvider.UpdateARecord(ip) if err != nil { - return fmt.Errorf("failed to update Namecheap DNS A record: %v", err) + return fmt.Errorf("failed to update Namecheap DNS A record: %w", err) } - fmt.Println("Namecheap DNS A record updated successfully.") + + utils.Log.Info().Str("level", "update").Msgf("Namecheap (%s.%s) | DNS A record updated successfully.", config.NamecheapSubdomain, config.NamecheapDomain) return nil } diff --git a/go.mod b/go.mod index f72ecba..bbd3f96 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,13 @@ module woodpecker go 1.22 -require github.com/robfig/cron/v3 v3.0.1 +require ( + github.com/robfig/cron/v3 v3.0.1 + github.com/rs/zerolog v1.33.0 +) + +require ( + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + golang.org/x/sys v0.12.0 // indirect +) diff --git a/go.sum b/go.sum index 0667807..bb2ed52 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,17 @@ +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/config/config.go b/internal/config/config.go index 7a2aa87..e9b9d80 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "strconv" + "woodpecker/internal/constants" ) @@ -45,7 +46,13 @@ func LoadConfig() (*Config, error) { } if config.IPService == "" { - return nil, fmt.Errorf("missing required IP Service field in %s", constants.ConfigFilename) + err = fmt.Errorf("missing required IP Service field in %s", constants.ConfigFilename) + return nil, err + } + + if config.PorkbunSecretKey == "" && config.NamecheapPassword == "" { + err = fmt.Errorf("no domain variables specified in %s, exiting", constants.ConfigFilename) + return nil, err } return config, nil diff --git a/internal/providers/namecheap/namecheap.go b/internal/providers/namecheap/namecheap.go index df00aab..c1fac68 100644 --- a/internal/providers/namecheap/namecheap.go +++ b/internal/providers/namecheap/namecheap.go @@ -5,6 +5,7 @@ import ( "io" "net/http" "strings" + "woodpecker/internal/config" "woodpecker/internal/providers" ) @@ -18,7 +19,7 @@ func New(config *config.Config) providers.DNSProvider { } func (n *Namecheap) GetCurrentARecord() (string, error) { - return "", fmt.Errorf("GetCurrentARecord not supported in namecheap via API access") + return "", fmt.Errorf("GetCurrentARecord not supported in namecheap without API access requirements: https://www.namecheap.com/support/knowledgebase/article.aspx/9739/63/api-faq/#c") } func (n *Namecheap) UpdateARecord(ip string) error { @@ -32,7 +33,7 @@ func (n *Namecheap) UpdateARecord(ip string) error { resp, err := http.Get(url) if err != nil { - return fmt.Errorf("failed to send update request to Namecheap: %v", err) + return fmt.Errorf("failed to send update request to Namecheap: %w", err) } defer resp.Body.Close() @@ -42,7 +43,7 @@ func (n *Namecheap) UpdateARecord(ip string) error { body, err := io.ReadAll(resp.Body) if err != nil { - return fmt.Errorf("failed to read response from Namecheap: %v", err) + return fmt.Errorf("failed to read response from Namecheap: %w", err) } if strings.Contains(string(body), "1") { diff --git a/internal/providers/porkbun/porkbun.go b/internal/providers/porkbun/porkbun.go index 65b169e..d8145d8 100644 --- a/internal/providers/porkbun/porkbun.go +++ b/internal/providers/porkbun/porkbun.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "net/http" + "woodpecker/internal/config" "woodpecker/internal/providers" ) @@ -45,7 +46,7 @@ func (p *Porkbun) GetCurrentARecord() (string, error) { resp, err := http.Post(url, "application/json", bytes.NewBuffer(bodyData)) if err != nil { - return "", fmt.Errorf("failed to send request: %v", err) + return "", fmt.Errorf("failed to send request: %w", err) } defer resp.Body.Close() @@ -56,7 +57,7 @@ func (p *Porkbun) GetCurrentARecord() (string, error) { var dnsResponse DNSResponse err = json.NewDecoder(resp.Body).Decode(&dnsResponse) if err != nil { - return "", fmt.Errorf("failed to parse DNS response: %v", err) + return "", fmt.Errorf("failed to parse DNS response: %w", err) } if len(dnsResponse.Records) == 0 { @@ -77,12 +78,12 @@ func (p *Porkbun) UpdateARecord(ip string) error { bodyData, err := json.Marshal(body) if err != nil { - return fmt.Errorf("failed to parse request body: %v", err) + return fmt.Errorf("failed to parse request body: %w", err) } resp, err := http.Post(url, "application/json", bytes.NewBuffer(bodyData)) if err != nil { - return fmt.Errorf("failed to send update request: %v", err) + return fmt.Errorf("failed to send update request: %w", err) } defer resp.Body.Close() @@ -93,11 +94,11 @@ func (p *Porkbun) UpdateARecord(ip string) error { var response map[string]interface{} err = json.NewDecoder(resp.Body).Decode(&response) if err != nil { - return fmt.Errorf("failed to parse DNS update response: %v", err) + return fmt.Errorf("failed to parse DNS update response: %w", err) } if response["status"] != "SUCCESS" { - return fmt.Errorf("failed to update DNS record, response: %v", response) + return fmt.Errorf("failed to update DNS record, response: %s", response) } return nil diff --git a/internal/services/ipservice.go b/internal/services/ipservice.go index 05f9e5d..5210307 100644 --- a/internal/services/ipservice.go +++ b/internal/services/ipservice.go @@ -5,19 +5,20 @@ import ( "io" "net/http" "strings" + "woodpecker/internal/config" ) func GetPublicIP(config *config.Config) (string, error) { resp, err := http.Get(config.IPService) if err != nil { - return "", fmt.Errorf("failed to retrieve IP from %s: %v", config.IPService, err) + return "", fmt.Errorf("failed to retrieve IP from "+config.IPService, err) } defer resp.Body.Close() ip, err := io.ReadAll(resp.Body) if err != nil { - return "", fmt.Errorf("failed to read response from %s: %v", config.IPService, err) + return "", fmt.Errorf("failed to read response from %s", err) } return strings.TrimSpace(string(ip)), nil diff --git a/internal/utils/utils.go b/internal/utils/io.go similarity index 67% rename from internal/utils/utils.go rename to internal/utils/io.go index f382484..d1dfd61 100644 --- a/internal/utils/utils.go +++ b/internal/utils/io.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "woodpecker/internal/constants" ) @@ -12,7 +13,7 @@ func GetAppPath() (string, error) { err := os.MkdirAll(dir, os.ModePerm) if err != nil { - return "", fmt.Errorf("failed to create directory: %v", err) + return "", fmt.Errorf("failed to create directory: %w", err) } return dir, nil @@ -22,12 +23,12 @@ func ReadIPFromFile(configPath string) (string, error) { ipFile := filepath.Join(configPath, constants.CurrentIpFilename) data, err := os.ReadFile(ipFile) if os.IsNotExist(err) { - fmt.Println("IP file does not exist yet, assuming first run") + Log.Info().Msg("IP file does not exist yet, assuming first run") return "", nil } if err != nil { - return "", fmt.Errorf("failed to read IP from file: %v", err) + return "", fmt.Errorf("failed to read IP from file: %w", err) } return string(data), nil @@ -37,8 +38,9 @@ func WriteIPToFile(ip, configPath string) error { ipFile := filepath.Join(configPath, constants.CurrentIpFilename) err := os.WriteFile(ipFile, []byte(ip), 0644) if err != nil { - return fmt.Errorf("failed to write IP to file: %v", err) + return fmt.Errorf("failed to write IP to file: %w", err) } + Log.Info().Str("level", "update").Msgf("stored IP address (%s) locally", ip) return nil } diff --git a/internal/utils/logger.go b/internal/utils/logger.go new file mode 100644 index 0000000..c5a20c5 --- /dev/null +++ b/internal/utils/logger.go @@ -0,0 +1,32 @@ +package utils + +import ( + "os" + "time" + + "github.com/rs/zerolog" +) + +var Log zerolog.Logger + +func InitLogger() { + zerolog.TimeFieldFormat = time.RFC3339 + + Log = zerolog.New(zerolog.ConsoleWriter{ + Out: os.Stdout, + TimeFormat: time.RFC3339, + FormatLevel: func(i interface{}) string { + level, ok := i.(string) + if ok && level == "update" { + return "| UPDATE |" + } + if ok && level == "error" { + return "| ERROR |" + } + return "| INFO |" + }, + FormatMessage: func(i interface{}) string { + return i.(string) + }, + }).With().Timestamp().Logger() +}