Docker template generator
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

dotege.go 6.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. package main
  2. import (
  3. "fmt"
  4. "github.com/csmith/dotege/model"
  5. "github.com/docker/docker/client"
  6. "github.com/xenolf/lego/certcrypto"
  7. "github.com/xenolf/lego/lego"
  8. "go.uber.org/zap"
  9. "go.uber.org/zap/zapcore"
  10. "io/ioutil"
  11. "os"
  12. "os/signal"
  13. "path"
  14. "strings"
  15. "syscall"
  16. "time"
  17. )
  18. const (
  19. envCertDestinationKey = "DOTEGE_CERT_DESTINATION"
  20. envCertDestinationDefault = "/data/certs/"
  21. envDnsProviderKey = "DOTEGE_DNS_PROVIDER"
  22. envAcmeEmailKey = "DOTEGE_ACME_EMAIL"
  23. envAcmeEndpointKey = "DOTEGE_ACME_ENDPOINT"
  24. envAcmeKeyTypeKey = "DOTEGE_ACME_KEY_TYPE"
  25. envAcmeKeyTypeDefault = "P384"
  26. envAcmeCacheLocationKey = "DOTEGE_ACME_CACHE_FILE"
  27. envAcmeCacheLocationDefault = "/data/config/certs.json"
  28. envTemplateDestinationKey = "DOTEGE_TEMPLATE_DESTINATION"
  29. envTemplateDestinationDefault = "/data/output/haproxy.cfg"
  30. envTemplateSourceKey = "DOTEGE_TEMPLATE_SOURCE"
  31. envTemplateSourceDefault = "./templates/haproxy.cfg.tpl"
  32. )
  33. var (
  34. logger *zap.SugaredLogger
  35. certificateManager *CertificateManager
  36. config *model.Config
  37. )
  38. func requiredVar(key string) (value string) {
  39. value, ok := os.LookupEnv(key)
  40. if !ok {
  41. panic(fmt.Errorf("required environmental variable not defined: %s", key))
  42. }
  43. return
  44. }
  45. func optionalVar(key string, fallback string) (value string) {
  46. value, ok := os.LookupEnv(key)
  47. if !ok {
  48. value = fallback
  49. }
  50. return
  51. }
  52. func monitorSignals() <-chan bool {
  53. signals := make(chan os.Signal, 1)
  54. done := make(chan bool, 1)
  55. signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
  56. go func() {
  57. sig := <-signals
  58. fmt.Printf("Received %s signal\n", sig)
  59. done <- true
  60. }()
  61. return done
  62. }
  63. func createLogger() *zap.SugaredLogger {
  64. zapConfig := zap.NewDevelopmentConfig()
  65. zapConfig.DisableCaller = true
  66. zapConfig.DisableStacktrace = true
  67. zapConfig.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
  68. zapConfig.OutputPaths = []string{"stdout"}
  69. zapConfig.ErrorOutputPaths = []string{"stdout"}
  70. logger, _ := zapConfig.Build()
  71. return logger.Sugar()
  72. }
  73. func createConfig() {
  74. config = &model.Config{
  75. Templates: []model.TemplateConfig{
  76. {
  77. Source: optionalVar(envTemplateSourceKey, envTemplateSourceDefault),
  78. Destination: optionalVar(envTemplateDestinationKey, envTemplateDestinationDefault),
  79. },
  80. },
  81. Labels: model.LabelConfig{
  82. Hostnames: "com.chameth.vhost",
  83. RequireAuth: "com.chameth.auth",
  84. },
  85. Acme: model.AcmeConfig{
  86. DnsProvider: requiredVar(envDnsProviderKey),
  87. Email: requiredVar(envAcmeEmailKey),
  88. Endpoint: optionalVar(envAcmeEndpointKey, lego.LEDirectoryProduction),
  89. KeyType: certcrypto.KeyType(optionalVar(envAcmeKeyTypeKey, envAcmeKeyTypeDefault)),
  90. CacheLocation: optionalVar(envAcmeCacheLocationKey, envAcmeCacheLocationDefault),
  91. },
  92. DefaultCertActions: model.COMBINE | model.FLATTEN,
  93. DefaultCertDestination: optionalVar(envCertDestinationKey, envCertDestinationDefault),
  94. }
  95. }
  96. func createTemplateGenerator(templates []model.TemplateConfig) *TemplateGenerator {
  97. templateGenerator := NewTemplateGenerator(logger)
  98. for _, template := range templates {
  99. templateGenerator.AddTemplate(template)
  100. }
  101. return templateGenerator
  102. }
  103. func createCertificateManager(config model.AcmeConfig) {
  104. certificateManager = NewCertificateManager(logger, config.Endpoint, config.KeyType, config.DnsProvider, config.CacheLocation)
  105. err := certificateManager.Init(config.Email)
  106. if err != nil {
  107. panic(err)
  108. }
  109. }
  110. func main() {
  111. logger = createLogger()
  112. logger.Info("Dotege is starting")
  113. doneChan := monitorSignals()
  114. createConfig()
  115. dockerStopChan := make(chan struct{})
  116. dockerClient, err := client.NewEnvClient()
  117. if err != nil {
  118. panic(err)
  119. }
  120. templateGenerator := createTemplateGenerator(config.Templates)
  121. createCertificateManager(config.Acme)
  122. jitterTimer := time.NewTimer(time.Minute)
  123. redeployTimer := time.NewTicker(time.Hour * 24)
  124. containers := make(map[string]*model.Container)
  125. go func() {
  126. err := monitorContainers(dockerClient, dockerStopChan, func(container *model.Container) {
  127. containers[container.Name] = container
  128. jitterTimer.Reset(100 * time.Millisecond)
  129. deployCertForContainer(container)
  130. }, func(name string) {
  131. delete(containers, name)
  132. jitterTimer.Reset(100 * time.Millisecond)
  133. })
  134. if err != nil {
  135. logger.Fatal("Error monitoring containers: ", err.Error())
  136. }
  137. }()
  138. go func() {
  139. for {
  140. select {
  141. case <-jitterTimer.C:
  142. hostnames := getHostnames(containers, *config)
  143. templateGenerator.Generate(Context{
  144. Containers: containers,
  145. Hostnames: hostnames,
  146. })
  147. case <-redeployTimer.C:
  148. logger.Info("Performing periodic certificate refresh")
  149. for _, container := range containers {
  150. deployCertForContainer(container)
  151. }
  152. }
  153. }
  154. }()
  155. <-doneChan
  156. dockerStopChan <- struct{}{}
  157. err = dockerClient.Close()
  158. if err != nil {
  159. panic(err)
  160. }
  161. }
  162. func getHostnamesForContainer(container *model.Container) []string {
  163. if label, ok := container.Labels[config.Labels.Hostnames]; ok {
  164. return strings.Split(strings.Replace(label, ",", " ", -1), " ")
  165. } else {
  166. return []string{}
  167. }
  168. }
  169. func getHostnames(containers map[string]*model.Container, config model.Config) (hostnames map[string]*model.Hostname) {
  170. hostnames = make(map[string]*model.Hostname)
  171. for _, container := range containers {
  172. if label, ok := container.Labels[config.Labels.Hostnames]; ok {
  173. names := strings.Split(strings.Replace(label, ",", " ", -1), " ")
  174. if hostname, ok := hostnames[names[0]]; ok {
  175. hostname.Containers = append(hostname.Containers, container)
  176. } else {
  177. hostnames[names[0]] = &model.Hostname{
  178. Name: names[0],
  179. Alternatives: make(map[string]bool),
  180. Containers: []*model.Container{container},
  181. CertActions: config.DefaultCertActions,
  182. CertDestination: config.DefaultCertDestination,
  183. }
  184. }
  185. addAlternatives(hostnames[names[0]], names[1:])
  186. if label, ok = container.Labels[config.Labels.RequireAuth]; ok {
  187. hostnames[names[0]].RequiresAuth = true
  188. hostnames[names[0]].AuthGroup = label
  189. }
  190. }
  191. }
  192. return
  193. }
  194. func addAlternatives(hostname *model.Hostname, alternatives []string) {
  195. for _, alternative := range alternatives {
  196. hostname.Alternatives[alternative] = true
  197. }
  198. }
  199. func deployCertForContainer(container *model.Container) {
  200. err, cert := certificateManager.GetCertificate(getHostnamesForContainer(container))
  201. if err != nil {
  202. logger.Warnf("Unable to generate certificate for %s: %s", container.Name, err.Error())
  203. } else {
  204. deployCert(cert)
  205. }
  206. }
  207. func deployCert(certificate *SavedCertificate) {
  208. target := path.Join(config.DefaultCertDestination, fmt.Sprintf("%s.pem", certificate.Domains[0]))
  209. err := ioutil.WriteFile(target, append(certificate.Certificate, certificate.PrivateKey...), 0700)
  210. if err != nil {
  211. logger.Warnf("Unable to write certificate %s - %s", target, err.Error())
  212. } else {
  213. logger.Infof("Updated certificate file %s", target)
  214. }
  215. }