package main import ( "encoding/json" "fmt" "github.com/robfig/cron" log "github.com/sirupsen/logrus" "github.com/urfave/cli" "gopkg.in/src-d/go-git.v4" "gopkg.in/src-d/go-git.v4/plumbing/object" githttp "gopkg.in/src-d/go-git.v4/plumbing/transport/http" "io/ioutil" "net/http" "net/smtp" "os" "os/signal" "path" "regexp" "strconv" "strings" "syscall" "time" ) type Configuration struct { RepositoryUrl string `json:"url"` Username string `json:"name"` Email string `json:"email"` GitLogin string `json:"login"` GitPassword string `json:"password"` } var ( configPath string scheduleSpec string ) var version = "master" var commit = "unknown" var date = "unknown" func main() { app := cli.NewApp() app.Name = "watchtower" app.Version = version + " - " + commit + " - " + date app.Usage = "Automatically update running Docker containers" app.Before = before app.Action = start app.Flags = []cli.Flag{ cli.StringFlag{ Name: "config, C", Usage: "path to config file", Value: "/config/conf.json", EnvVar: "CONFIG_PATH", }, cli.IntFlag{ Name: "interval, i", Usage: "poll interval (in seconds)", Value: 300, EnvVar: "DNSUPDATER_POLL_INTERVAL", }, cli.StringFlag{ Name: "schedule, s", Usage: "the cron expression which defines when to update", EnvVar: "DNSUPDATER_SCHEDULE", }, } if err := app.Run(os.Args); err != nil { log.Fatal(err) } } func before(c *cli.Context) error { if c.GlobalBool("debug") { log.SetLevel(log.DebugLevel) } pollingSet := c.IsSet("interval") cronSet := c.IsSet("schedule") if pollingSet && cronSet { log.Fatal("Only schedule or interval can be defined, not both.") } else if cronSet { scheduleSpec = c.String("schedule") } else { scheduleSpec = "@every " + strconv.Itoa(c.Int("interval")) + "s" } return nil } func start(c *cli.Context) error { tryLockSem := make(chan bool, 1) tryLockSem <- true cron := cron.New() err := cron.AddFunc( scheduleSpec, func() { select { case v := <-tryLockSem: defer func() { tryLockSem <- v }() if err := update(c); err != nil { log.Println(err) SendStatusMail(fmt.Sprintf("Error during update process : %s", err)) } default: log.Debug("Skipped another update already running.") } nextRuns := cron.Entries() if len(nextRuns) > 0 { log.Debug("Scheduled next run: " + nextRuns[0].Next.String()) } }) if err != nil { return err } log.Info("First run: " + cron.Entries()[0].Schedule.Next(time.Now()).String()) cron.Start() // Graceful shut-down on SIGINT/SIGTERM interrupt := make(chan os.Signal, 1) signal.Notify(interrupt, os.Interrupt) signal.Notify(interrupt, syscall.SIGTERM) <-interrupt cron.Stop() log.Info("Waiting for running update to be finished...") <-tryLockSem os.Exit(1) return nil } func update(c *cli.Context) error { Info("Running dnsupdater...") configPath = c.String("config") if confPathExists, _ := exists(configPath); !confPathExists { configPath = "./data"; } file, _ := os.Open(path.Join(configPath, "conf.json")) defer file.Close() decoder := json.NewDecoder(file) configuration := Configuration{} err := decoder.Decode(&configuration) Info("Repository : %s", configuration.RepositoryUrl) Info("Name : %s", configuration.Username) Info("Email : %s", configuration.Email) Info("GitLogin : %s", configuration.GitLogin) err = os.RemoveAll(path.Join(configPath, "data")) if err != nil { return err } _, err = git.PlainClone(path.Join(configPath, "data"), false, &git.CloneOptions{ URL: configuration.RepositoryUrl, Progress: os.Stdout, }) if err != nil { return err } currentIp, err := GetCurrentIp() if err != nil { return err } newIp, err := GetOutboundIP() if err != nil { return err } if currentIp == newIp { Info("Ip has not changed %s", currentIp) return nil } Info("Ip has changed %s -> %s", currentIp, newIp) SetCurrentIp(currentIp, newIp) repo, err := git.PlainOpen(path.Join(configPath, "data")) if err != nil { return err } w, err := repo.Worktree() if err != nil { return err } _, err = w.Add("templates/home_a.lua") if err != nil { return err } status, err := w.Status() if err != nil { return err } fmt.Println(status) _, err = w.Commit(fmt.Sprintf("Changement d'IP maison : %s -> %s", currentIp, newIp), &git.CommitOptions{ Author: &object.Signature{ Name: configuration.Username, Email: configuration.Email, When: time.Now().Local(), }, }) if err != nil { return err } err = repo.Push(&git.PushOptions{ Auth: &githttp.BasicAuth{ Username: configuration.GitLogin, Password: configuration.GitPassword, }, }) if err != nil { return err } SendStatusMail(fmt.Sprintf(`Home IP has changed : %s -> %s Dns updated successfully`, currentIp, newIp)) return nil } func SendStatusMail(messageText string) { // user we are authorizing as from := "laurent@lehouerou.net" // use we are sending email to to := "laurent@lehouerou.net" // server we are authorized to send email through host := "smtp.fastmail.com" // Create the authentication for the SendMail() // using PlainText, but other authentication methods are encouraged auth := smtp.PlainAuth("", from, "c9bd8fb8l4bhs2f8", host) // NOTE: Using the backtick here ` works like a heredoc, which is why all the // rest of the lines are forced to the beginning of the line, otherwise the // formatting is wrong for the RFC 822 style message := fmt.Sprintf(`To: "Laurent Le Houerou" From: "Laurent Le Houerou" Subject: DnsUpdater Status %s `,messageText) if err := smtp.SendMail(host+":587", auth, from, []string{to}, []byte(message)); err != nil { fmt.Println("Error SendMail: ", err) } else { fmt.Println("Email Sent!") } } func GetCurrentIp() (string, error) { b, err := ioutil.ReadFile(path.Join(configPath, "data/templates/home_a.lua")) if err != nil { return "", err } re := regexp.MustCompile("\"(.*?)\"") match := re.FindStringSubmatch(string(b)) return match[1], nil } func SetCurrentIp(oldip string, newip string) error { b, err := ioutil.ReadFile(path.Join(configPath, "data/templates/home_a.lua")) if err != nil { return err } s := string(b) s = strings.Replace(s, oldip, newip, 1) ioutil.WriteFile(path.Join(configPath, "data/templates/home_a.lua"), []byte(s), 0644) return nil } func GetOutboundIP() (string, error) { url := "https://diagnostic.opendns.com/myip" req, err := http.NewRequest("GET", url, nil) if err != nil { return "", err } res, err := http.DefaultClient.Do(req) if err != nil { return "", err } defer res.Body.Close() body, err := ioutil.ReadAll(res.Body) if err != nil { return "", err } return string(body), nil } func exists(path string) (bool, error) { _, err := os.Stat(path) if err == nil { return true, nil } if os.IsNotExist(err) { return false, nil } return true, err } func Info(format string, args ...interface{}) { fmt.Printf("\x1b[34;1m%s\x1b[0m\n", fmt.Sprintf(format, args...)) }