diff --git a/cmd/kv.go b/cmd/kv.go index fd5f493..031e56b 100644 --- a/cmd/kv.go +++ b/cmd/kv.go @@ -1,7 +1,6 @@ package cmd import ( - "strconv" "time" "github.com/abronan/valkeyrie/store" @@ -24,7 +23,6 @@ func init() { kvCmd.PersistentFlags().String("prefix", "traefik", "Prefix used for KV store.") kvCmd.PersistentFlags().String("password", "", "Password for connection.") kvCmd.PersistentFlags().String("username", "", "Username for connection.") - kvCmd.PersistentFlags().Bool("watch", false, "Enable watching changes.") // FIXME review TLS parts // kvCmd.PersistentFlags().Bool("tls.enable", false, "Enable TLS encryption.") @@ -43,8 +41,6 @@ func getKvConfig(cmd *cobra.Command) (*kv.Config, error) { return nil, err } - watch, _ := strconv.ParseBool(cmd.Flag("watch").Value.String()) - return &kv.Config{ Endpoints: endpoints, Prefix: cmd.Flag("prefix").Value.String(), @@ -53,6 +49,5 @@ func getKvConfig(cmd *cobra.Command) (*kv.Config, error) { Username: cmd.Flag("password").Value.String(), Password: cmd.Flag("username").Value.String(), }, - Watch: watch, }, nil } diff --git a/cmd/root.go b/cmd/root.go index ef52ff7..a67b5f5 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -60,6 +60,7 @@ func init() { rootCmd.PersistentFlags().String("key-name", "privatekey", "The file name (without extension) of the generated private keys.") rootCmd.PersistentFlags().Bool("domain-subdir", false, "Use domain as sub-directory.") rootCmd.PersistentFlags().Bool("clean", true, "Clean destination folder before dumping content.") + rootCmd.PersistentFlags().Bool("watch", false, "Enable watching changes.") } // initConfig reads in config file and ENV variables if set. @@ -88,32 +89,6 @@ func initConfig() { } } -func getBaseConfig(cmd *cobra.Command) (*dumper.BaseConfig, error) { - subDir, err := strconv.ParseBool(cmd.Flag("domain-subdir").Value.String()) - if err != nil { - return nil, err - } - - clean, err := strconv.ParseBool(cmd.Flag("clean").Value.String()) - if err != nil { - return nil, err - } - - return &dumper.BaseConfig{ - DumpPath: cmd.Flag("dest").Value.String(), - CrtInfo: dumper.FileInfo{ - Name: cmd.Flag("crt-name").Value.String(), - Ext: cmd.Flag("crt-ext").Value.String(), - }, - KeyInfo: dumper.FileInfo{ - Name: cmd.Flag("key-name").Value.String(), - Ext: cmd.Flag("key-ext").Value.String(), - }, - DomainSubDir: subDir, - Clean: clean, - }, nil -} - func runE(apply func(*dumper.BaseConfig, *cobra.Command) error) func(*cobra.Command, []string) error { return func(cmd *cobra.Command, _ []string) error { baseConfig, err := getBaseConfig(cmd) @@ -169,3 +144,35 @@ func tree(root, indent string) error { return nil } + +func getBaseConfig(cmd *cobra.Command) (*dumper.BaseConfig, error) { + subDir, err := strconv.ParseBool(cmd.Flag("domain-subdir").Value.String()) + if err != nil { + return nil, err + } + + clean, err := strconv.ParseBool(cmd.Flag("clean").Value.String()) + if err != nil { + return nil, err + } + + watch, err := strconv.ParseBool(cmd.Flag("watch").Value.String()) + if err != nil { + return nil, err + } + + return &dumper.BaseConfig{ + DumpPath: cmd.Flag("dest").Value.String(), + CrtInfo: dumper.FileInfo{ + Name: cmd.Flag("crt-name").Value.String(), + Ext: cmd.Flag("crt-ext").Value.String(), + }, + KeyInfo: dumper.FileInfo{ + Name: cmd.Flag("key-name").Value.String(), + Ext: cmd.Flag("key-ext").Value.String(), + }, + DomainSubDir: subDir, + Clean: clean, + Watch: watch, + }, nil +} diff --git a/docs/traefik-certs-dumper.md b/docs/traefik-certs-dumper.md index 4790da9..da0c103 100644 --- a/docs/traefik-certs-dumper.md +++ b/docs/traefik-certs-dumper.md @@ -9,6 +9,7 @@ Dump Let's Encrypt certificates from Traefik. ### Options ``` + --clean Clean destination folder before dumping content. (default true) --config string config file (default is $HOME/.traefik-certs-dumper.yaml) --crt-ext string The file extension of the generated certificates. (default ".crt") --crt-name string The file name (without extension) of the generated certificates. (default "certificate") @@ -17,6 +18,7 @@ Dump Let's Encrypt certificates from Traefik. -h, --help help for traefik-certs-dumper --key-ext string The file extension of the generated private keys. (default ".key") --key-name string The file name (without extension) of the generated private keys. (default "privatekey") + --watch Enable watching changes. ``` ### SEE ALSO @@ -25,4 +27,4 @@ Dump Let's Encrypt certificates from Traefik. * [traefik-certs-dumper kv](traefik-certs-dumper_kv.md) - Dump the content of a KV store. * [traefik-certs-dumper version](traefik-certs-dumper_version.md) - Display version -###### Auto generated by spf13/cobra on 20-Apr-2019 +###### Auto generated by spf13/cobra on 22-Apr-2019 diff --git a/docs/traefik-certs-dumper_file.md b/docs/traefik-certs-dumper_file.md index 5c3dc50..a4a7f85 100644 --- a/docs/traefik-certs-dumper_file.md +++ b/docs/traefik-certs-dumper_file.md @@ -20,6 +20,7 @@ traefik-certs-dumper file [flags] ### Options inherited from parent commands ``` + --clean Clean destination folder before dumping content. (default true) --config string config file (default is $HOME/.traefik-certs-dumper.yaml) --crt-ext string The file extension of the generated certificates. (default ".crt") --crt-name string The file name (without extension) of the generated certificates. (default "certificate") @@ -27,10 +28,11 @@ traefik-certs-dumper file [flags] --domain-subdir Use domain as sub-directory. --key-ext string The file extension of the generated private keys. (default ".key") --key-name string The file name (without extension) of the generated private keys. (default "privatekey") + --watch Enable watching changes. ``` ### SEE ALSO * [traefik-certs-dumper](traefik-certs-dumper.md) - Dump Let's Encrypt certificates from Traefik. -###### Auto generated by spf13/cobra on 20-Apr-2019 +###### Auto generated by spf13/cobra on 22-Apr-2019 diff --git a/docs/traefik-certs-dumper_kv.md b/docs/traefik-certs-dumper_kv.md index 3343203..ff66cee 100644 --- a/docs/traefik-certs-dumper_kv.md +++ b/docs/traefik-certs-dumper_kv.md @@ -15,12 +15,12 @@ Dump the content of a KV store. --password string Password for connection. --prefix string Prefix used for KV store. (default "traefik") --username string Username for connection. - --watch Enable watching changes. ``` ### Options inherited from parent commands ``` + --clean Clean destination folder before dumping content. (default true) --config string config file (default is $HOME/.traefik-certs-dumper.yaml) --crt-ext string The file extension of the generated certificates. (default ".crt") --crt-name string The file name (without extension) of the generated certificates. (default "certificate") @@ -28,6 +28,7 @@ Dump the content of a KV store. --domain-subdir Use domain as sub-directory. --key-ext string The file extension of the generated private keys. (default ".key") --key-name string The file name (without extension) of the generated private keys. (default "privatekey") + --watch Enable watching changes. ``` ### SEE ALSO @@ -38,4 +39,4 @@ Dump the content of a KV store. * [traefik-certs-dumper kv etcd](traefik-certs-dumper_kv_etcd.md) - Dump the content of etcd. * [traefik-certs-dumper kv zookeeper](traefik-certs-dumper_kv_zookeeper.md) - Dump the content of zookeeper. -###### Auto generated by spf13/cobra on 20-Apr-2019 +###### Auto generated by spf13/cobra on 22-Apr-2019 diff --git a/docs/traefik-certs-dumper_kv_boltdb.md b/docs/traefik-certs-dumper_kv_boltdb.md index d4d1d71..bfc8cfe 100644 --- a/docs/traefik-certs-dumper_kv_boltdb.md +++ b/docs/traefik-certs-dumper_kv_boltdb.md @@ -21,6 +21,7 @@ traefik-certs-dumper kv boltdb [flags] ### Options inherited from parent commands ``` + --clean Clean destination folder before dumping content. (default true) --config string config file (default is $HOME/.traefik-certs-dumper.yaml) --connection-timeout int Connection timeout in seconds. --crt-ext string The file extension of the generated certificates. (default ".crt") @@ -40,4 +41,4 @@ traefik-certs-dumper kv boltdb [flags] * [traefik-certs-dumper kv](traefik-certs-dumper_kv.md) - Dump the content of a KV store. -###### Auto generated by spf13/cobra on 20-Apr-2019 +###### Auto generated by spf13/cobra on 22-Apr-2019 diff --git a/docs/traefik-certs-dumper_kv_consul.md b/docs/traefik-certs-dumper_kv_consul.md index ce9250c..b0a5e4c 100644 --- a/docs/traefik-certs-dumper_kv_consul.md +++ b/docs/traefik-certs-dumper_kv_consul.md @@ -20,6 +20,7 @@ traefik-certs-dumper kv consul [flags] ### Options inherited from parent commands ``` + --clean Clean destination folder before dumping content. (default true) --config string config file (default is $HOME/.traefik-certs-dumper.yaml) --connection-timeout int Connection timeout in seconds. --crt-ext string The file extension of the generated certificates. (default ".crt") @@ -39,4 +40,4 @@ traefik-certs-dumper kv consul [flags] * [traefik-certs-dumper kv](traefik-certs-dumper_kv.md) - Dump the content of a KV store. -###### Auto generated by spf13/cobra on 20-Apr-2019 +###### Auto generated by spf13/cobra on 22-Apr-2019 diff --git a/docs/traefik-certs-dumper_kv_etcd.md b/docs/traefik-certs-dumper_kv_etcd.md index b7077c0..236d1b9 100644 --- a/docs/traefik-certs-dumper_kv_etcd.md +++ b/docs/traefik-certs-dumper_kv_etcd.md @@ -20,6 +20,7 @@ traefik-certs-dumper kv etcd [flags] ### Options inherited from parent commands ``` + --clean Clean destination folder before dumping content. (default true) --config string config file (default is $HOME/.traefik-certs-dumper.yaml) --connection-timeout int Connection timeout in seconds. --crt-ext string The file extension of the generated certificates. (default ".crt") @@ -39,4 +40,4 @@ traefik-certs-dumper kv etcd [flags] * [traefik-certs-dumper kv](traefik-certs-dumper_kv.md) - Dump the content of a KV store. -###### Auto generated by spf13/cobra on 20-Apr-2019 +###### Auto generated by spf13/cobra on 22-Apr-2019 diff --git a/docs/traefik-certs-dumper_kv_zookeeper.md b/docs/traefik-certs-dumper_kv_zookeeper.md index 9e275b6..f2837f6 100644 --- a/docs/traefik-certs-dumper_kv_zookeeper.md +++ b/docs/traefik-certs-dumper_kv_zookeeper.md @@ -19,6 +19,7 @@ traefik-certs-dumper kv zookeeper [flags] ### Options inherited from parent commands ``` + --clean Clean destination folder before dumping content. (default true) --config string config file (default is $HOME/.traefik-certs-dumper.yaml) --connection-timeout int Connection timeout in seconds. --crt-ext string The file extension of the generated certificates. (default ".crt") @@ -38,4 +39,4 @@ traefik-certs-dumper kv zookeeper [flags] * [traefik-certs-dumper kv](traefik-certs-dumper_kv.md) - Dump the content of a KV store. -###### Auto generated by spf13/cobra on 20-Apr-2019 +###### Auto generated by spf13/cobra on 22-Apr-2019 diff --git a/docs/traefik-certs-dumper_version.md b/docs/traefik-certs-dumper_version.md index 2a40ad3..caa7e2c 100644 --- a/docs/traefik-certs-dumper_version.md +++ b/docs/traefik-certs-dumper_version.md @@ -19,6 +19,7 @@ traefik-certs-dumper version [flags] ### Options inherited from parent commands ``` + --clean Clean destination folder before dumping content. (default true) --config string config file (default is $HOME/.traefik-certs-dumper.yaml) --crt-ext string The file extension of the generated certificates. (default ".crt") --crt-name string The file name (without extension) of the generated certificates. (default "certificate") @@ -26,10 +27,11 @@ traefik-certs-dumper version [flags] --domain-subdir Use domain as sub-directory. --key-ext string The file extension of the generated private keys. (default ".key") --key-name string The file name (without extension) of the generated private keys. (default "privatekey") + --watch Enable watching changes. ``` ### SEE ALSO * [traefik-certs-dumper](traefik-certs-dumper.md) - Dump Let's Encrypt certificates from Traefik. -###### Auto generated by spf13/cobra on 20-Apr-2019 +###### Auto generated by spf13/cobra on 22-Apr-2019 diff --git a/dumper/config.go b/dumper/config.go index cd6c0de..1eb1669 100644 --- a/dumper/config.go +++ b/dumper/config.go @@ -7,4 +7,5 @@ type BaseConfig struct { KeyInfo FileInfo DomainSubDir bool Clean bool + Watch bool } diff --git a/dumper/file/file.go b/dumper/file/file.go index 2ecee24..f230b78 100644 --- a/dumper/file/file.go +++ b/dumper/file/file.go @@ -1,14 +1,32 @@ package file import ( + "bytes" + "crypto/md5" "encoding/json" + "io" + "log" "os" + "strings" + "github.com/fsnotify/fsnotify" "github.com/ldez/traefik-certs-dumper/v2/dumper" ) // Dump Dumps "acme.json" file to certificates. func Dump(acmeFile string, baseConfig *dumper.BaseConfig) error { + err := dump(acmeFile, baseConfig) + if err != nil { + return err + } + + if baseConfig.Watch { + return watch(acmeFile, baseConfig) + } + return nil +} + +func dump(acmeFile string, baseConfig *dumper.BaseConfig) error { data, err := readFile(acmeFile) if err != nil { return err @@ -30,3 +48,115 @@ func readFile(acmeFile string) (*dumper.StoredData, error) { return data, nil } + +func watch(acmeFile string, baseConfig *dumper.BaseConfig) error { + watcher, err := fsnotify.NewWatcher() + if err != nil { + return err + } + + defer func() { _ = watcher.Close() }() + + done := make(chan bool) + go func() { + var previousHash []byte + + for { + select { + case event, ok := <-watcher.Events: + if !ok { + return + } + + if strings.EqualFold(os.Getenv("TCD_DEBUG"), "true") { + log.Println("event:", event) + } + + hash, errW := manageEvent(watcher, event, acmeFile, previousHash, baseConfig) + if errW != nil { + log.Println("error:", errW) + done <- true + return + } + + previousHash = hash + + case errW, ok := <-watcher.Errors: + if !ok { + return + } + + log.Println("error:", errW) + done <- true + return + } + } + }() + + err = watcher.Add(acmeFile) + if err != nil { + return err + } + + <-done + + return nil +} + +func manageEvent(watcher *fsnotify.Watcher, event fsnotify.Event, acmeFile string, previousHash []byte, baseConfig *dumper.BaseConfig) ([]byte, error) { + err := manageRename(watcher, event, acmeFile) + if err != nil { + return nil, err + } + + hash, err := calculateHash(acmeFile) + if err != nil { + return nil, err + } + + if !bytes.Equal(previousHash, hash) { + if strings.EqualFold(os.Getenv("TCD_DEBUG"), "true") { + log.Println("detected changes on file:", event.Name) + } + + if errD := dump(acmeFile, baseConfig); errD != nil { + return nil, errD + } + + log.Println("Dumped new certificate data.") + } + + return hash, nil +} + +func manageRename(watcher *fsnotify.Watcher, event fsnotify.Event, acmeFile string) error { + if event.Op&fsnotify.Rename == fsnotify.Rename { + err := watcher.Remove(acmeFile) + if err != nil { + return err + } + + err = watcher.Add(acmeFile) + if err != nil { + return err + } + } + + return nil +} + +func calculateHash(acmeFile string) ([]byte, error) { + file, err := os.Open(acmeFile) + if err != nil { + return nil, err + } + defer func() { _ = file.Close() }() + + h := md5.New() + _, err = io.Copy(h, file) + if err != nil { + return nil, err + } + + return h.Sum(nil), nil +} diff --git a/dumper/kv/config.go b/dumper/kv/config.go index f2f5e10..76d0e89 100644 --- a/dumper/kv/config.go +++ b/dumper/kv/config.go @@ -7,6 +7,5 @@ type Config struct { Backend store.Backend Prefix string Endpoints []string - Watch bool Options *store.Config } diff --git a/dumper/kv/kv.go b/dumper/kv/kv.go index 32075a9..d9d7cf6 100644 --- a/dumper/kv/kv.go +++ b/dumper/kv/kv.go @@ -24,7 +24,7 @@ func Dump(config *Config, baseConfig *dumper.BaseConfig) error { storeKey := config.Prefix + storeKeySuffix - if config.Watch { + if baseConfig.Watch { return watch(kvStore, storeKey, baseConfig) } diff --git a/go.mod b/go.mod index f02c99f..6f7e14a 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f // indirect github.com/cpuguy83/go-md2man v1.0.10 // indirect github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect + github.com/fsnotify/fsnotify v1.4.7 github.com/go-acme/lego v2.5.0+incompatible github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef // indirect github.com/google/btree v1.0.0 // indirect