diff --git a/.gitignore b/.gitignore index f2cc188..c367738 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ *.out .idea/ +/main diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ca6fa57 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +# +# Alpine image to get some needed data +# +FROM alpine:latest as alpine +RUN apk add --no-cache \ + ca-certificates \ + tzdata + +# +# Image +# +FROM scratch + +# copy files from other containers +COPY --from=alpine /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt +COPY --from=alpine /usr/share/zoneinfo /usr/share/zoneinfo + +COPY main / +ENTRYPOINT ["/main"] \ No newline at end of file diff --git a/main.go b/main.go index 59fe270..afcd0eb 100644 --- a/main.go +++ b/main.go @@ -3,73 +3,200 @@ 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" "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"` + Username string `json:"name"` + Email string `json:"email"` + GitLogin string `json:"login"` + GitPassword string `json:"password"` } -var configPath string +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) + + } + 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 = os.Args[1] + configPath = c.String("config") - if confPathExists, _ := exists(configPath); !confPathExists{ + if confPathExists, _ := exists(configPath); !confPathExists { configPath = "./data"; } - file, _ := os.Open(path.Join(configPath,"conf.json")) + file, _ := os.Open(path.Join(configPath, "conf.json")) defer file.Close() decoder := json.NewDecoder(file) configuration := Configuration{} err := decoder.Decode(&configuration) - err = os.RemoveAll(path.Join(configPath,"data")) - CheckIfError(err) + err = os.RemoveAll(path.Join(configPath, "data")) + if err != nil { + return err + } - _, err = git.PlainClone(path.Join(configPath,"data"), false, &git.CloneOptions{ + _, err = git.PlainClone(path.Join(configPath, "data"), false, &git.CloneOptions{ URL: configuration.RepositoryUrl, Progress: os.Stdout, }) - CheckIfError(err) + if err != nil { + return err + } - currentIp := GetCurrentIp() - newIp := GetOutboundIP() + 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) - os.Exit(0) + return nil } Info("Ip has changed %s -> %s", currentIp, newIp) SetCurrentIp(currentIp, newIp) - repo, err := git.PlainOpen(path.Join(configPath,"data")) - CheckIfError(err) + repo, err := git.PlainOpen(path.Join(configPath, "data")) + if err != nil { + return err + } w, err := repo.Worktree() - CheckIfError(err) + if err != nil { + return err + } _, err = w.Add("templates/home_a.lua") - CheckIfError(err) + if err != nil { + return err + } status, err := w.Status() - CheckIfError(err) + if err != nil { + return err + } fmt.Println(status) _, err = w.Commit(fmt.Sprintf("Changement d'IP maison : %s -> %s", currentIp, newIp), &git.CommitOptions{ @@ -79,7 +206,9 @@ func main() { When: time.Now().Local(), }, }) - CheckIfError(err) + if err != nil { + return err + } err = repo.Push(&git.PushOptions{ Auth: &githttp.BasicAuth{ @@ -87,47 +216,51 @@ func main() { Password: configuration.GitPassword, }, }) - CheckIfError(err) - + if err != nil { + return err + } + return nil } -func GetCurrentIp() string { - b, err := ioutil.ReadFile(path.Join(configPath,"data/templates/home_a.lua")) - CheckIfError(err) +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] + return match[1], nil } -func SetCurrentIp(oldip string, newip string) { - b, err := ioutil.ReadFile(path.Join(configPath,"data/templates/home_a.lua")) - CheckIfError(err) +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) + ioutil.WriteFile(path.Join(configPath, "data/templates/home_a.lua"), []byte(s), 0644) + return nil } -func CheckIfError(err error) { - if err == nil { - return - } - - fmt.Printf("\x1b[31;1m%s\x1b[0m\n", fmt.Sprintf("error: %s", err)) - os.Exit(1) -} - -func GetOutboundIP() string { +func GetOutboundIP() (string, error) { url := "https://diagnostic.opendns.com/myip" req, err := http.NewRequest("GET", url, nil) - CheckIfError(err) + if err != nil { + return "", err + } res, err := http.DefaultClient.Do(req) - CheckIfError(err) + if err != nil { + return "", err + } defer res.Body.Close() body, err := ioutil.ReadAll(res.Body) - CheckIfError(err) - return string(body) + if err != nil { + return "", err + } + return string(body), nil } func exists(path string) (bool, error) {