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 8.5KB

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