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

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