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.3KB

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