123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286 |
- package main
-
- import (
- "bytes"
- "context"
- "fmt"
- "github.com/docker/docker/client"
- "go.uber.org/zap"
- "go.uber.org/zap/zapcore"
- "io/ioutil"
- "os"
- "os/signal"
- "path"
- "strings"
- "syscall"
- "time"
- )
-
- // Container models a docker container that is running on the system.
- type Container struct {
- Id string
- Name string
- Labels map[string]string
- }
-
- // Hostname describes a DNS name used for proxying, retrieving certificates, etc.
- type Hostname struct {
- Name string
- Alternatives map[string]bool
- Containers []*Container
- CertDestination string
- RequiresAuth bool
- AuthGroup string
- }
-
- var (
- logger *zap.SugaredLogger
- certificateManager *CertificateManager
- dockerClient *client.Client
- config *Config
- containers = make(map[string]*Container)
- )
-
- func monitorSignals() <-chan bool {
- signals := make(chan os.Signal, 1)
- done := make(chan bool, 1)
-
- signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
-
- go func() {
- sig := <-signals
- fmt.Printf("Received %s signal\n", sig)
- done <- true
- }()
-
- return done
- }
-
- func createLogger() *zap.SugaredLogger {
- zapConfig := zap.NewDevelopmentConfig()
- zapConfig.DisableCaller = true
- zapConfig.DisableStacktrace = true
- zapConfig.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
- zapConfig.OutputPaths = []string{"stdout"}
- zapConfig.ErrorOutputPaths = []string{"stdout"}
- logger, _ := zapConfig.Build()
- return logger.Sugar()
- }
-
- func createTemplateGenerator(templates []TemplateConfig) *TemplateGenerator {
- templateGenerator := NewTemplateGenerator()
- for _, template := range templates {
- templateGenerator.AddTemplate(template)
- }
- return templateGenerator
- }
-
- func createCertificateManager(config AcmeConfig) {
- certificateManager = NewCertificateManager(logger, config.Endpoint, config.KeyType, config.DnsProvider, config.CacheLocation)
- err := certificateManager.Init(config.Email)
- if err != nil {
- panic(err)
- }
- }
-
- func main() {
- logger = createLogger()
- logger.Info("Dotege is starting")
-
- doneChan := monitorSignals()
- config = createConfig()
-
- var err error
- dockerStopChan := make(chan struct{})
- dockerClient, err = client.NewEnvClient()
- if err != nil {
- panic(err)
- }
-
- templateGenerator := createTemplateGenerator(config.Templates)
- createCertificateManager(config.Acme)
-
- jitterTimer := time.NewTimer(time.Minute)
- redeployTimer := time.NewTicker(time.Hour * 24)
- updatedContainers := make(map[string]*Container)
-
- go func() {
- err := monitorContainers(dockerClient, dockerStopChan, func(container *Container) {
- containers[container.Name] = container
- updatedContainers[container.Name] = container
- jitterTimer.Reset(100 * time.Millisecond)
- }, func(name string) {
- delete(updatedContainers, name)
- delete(containers, name)
- jitterTimer.Reset(100 * time.Millisecond)
- })
-
- if err != nil {
- logger.Fatal("Error monitoring containers: ", err.Error())
- }
- }()
-
- go func() {
- for {
- select {
- case <-jitterTimer.C:
- hostnames := getHostnames(containers)
- updated := templateGenerator.Generate(Context{
- Containers: containers,
- Hostnames: hostnames,
- })
-
- for name, container := range updatedContainers {
- certDeployed := deployCertForContainer(container)
- updated = updated || certDeployed
- delete(updatedContainers, name)
- }
-
- if updated {
- signalContainer()
- }
- case <-redeployTimer.C:
- logger.Info("Performing periodic certificate refresh")
- for _, container := range containers {
- deployCertForContainer(container)
- signalContainer()
- }
- }
- }
- }()
-
- <-doneChan
-
- dockerStopChan <- struct{}{}
- err = dockerClient.Close()
- if err != nil {
- panic(err)
- }
- }
-
- func signalContainer() {
- for _, s := range config.Signals {
- container, ok := containers[s.Name]
- if ok {
- logger.Debugf("Killing container %s with signal %s", s.Name, s.Signal)
- err := dockerClient.ContainerKill(context.Background(), container.Id, s.Signal)
- if err != nil {
- logger.Errorf("Unable to send signal %s to container %s: %s", s.Signal, s.Name, err.Error())
- }
- } else {
- logger.Warnf("Couldn't signal container %s as it is not running", s.Name)
- }
- }
- }
-
- func getHostnamesForContainer(container *Container) []string {
- if label, ok := container.Labels[config.Labels.Hostnames]; ok {
- return applyWildcards(splitList(label), config.WildCardDomains)
- } else {
- return []string{}
- }
- }
-
- func applyWildcards(domains []string, wildcards []string) (result []string) {
- result = []string{}
- required := make(map[string]bool)
- for _, domain := range domains {
- found := false
- for _, wildcard := range wildcards {
- if wildcardMatches(wildcard, domain) {
- if !required["*."+wildcard] {
- result = append(result, "*."+wildcard)
- required["*."+wildcard] = true
- }
- found = true
- break
- }
- }
-
- if !found && !required[domain] {
- result = append(result, domain)
- required[domain] = true
- }
- }
- return
- }
-
- func wildcardMatches(wildcard, domain string) bool {
- if len(domain) <= len(wildcard) {
- return false
- }
-
- pivot := len(domain) - len(wildcard) - 1
- start := domain[:pivot]
- end := domain[pivot+1:]
- return domain[pivot] == '.' && end == wildcard && !strings.ContainsRune(start, '.')
- }
-
- func getHostnames(containers map[string]*Container) (hostnames map[string]*Hostname) {
- hostnames = make(map[string]*Hostname)
- for _, container := range containers {
- if label, ok := container.Labels[config.Labels.Hostnames]; ok {
- names := splitList(label)
- if hostname, ok := hostnames[names[0]]; ok {
- hostname.Containers = append(hostname.Containers, container)
- } else {
- hostnames[names[0]] = &Hostname{
- Name: names[0],
- Alternatives: make(map[string]bool),
- Containers: []*Container{container},
- CertDestination: config.DefaultCertDestination,
- }
- }
- addAlternatives(hostnames[names[0]], names[1:])
-
- if label, ok = container.Labels[config.Labels.RequireAuth]; ok {
- hostnames[names[0]].RequiresAuth = true
- hostnames[names[0]].AuthGroup = label
- }
- }
- }
- return
- }
-
- func addAlternatives(hostname *Hostname, alternatives []string) {
- for _, alternative := range alternatives {
- hostname.Alternatives[alternative] = true
- }
- }
-
- func deployCertForContainer(container *Container) bool {
- hostnames := getHostnamesForContainer(container)
- if len(hostnames) == 0 {
- logger.Debugf("No labels found for container %s", container.Name)
- return false
- }
-
- err, cert := certificateManager.GetCertificate(hostnames)
- if err != nil {
- logger.Warnf("Unable to generate certificate for %s: %s", container.Name, err.Error())
- return false
- } else {
- return deployCert(cert)
- }
- }
-
- func deployCert(certificate *SavedCertificate) bool {
- name := fmt.Sprintf("%s.pem", strings.ReplaceAll(certificate.Domains[0], "*", "_"))
- target := path.Join(config.DefaultCertDestination, name)
- content := append(certificate.Certificate, certificate.PrivateKey...)
-
- buf, _ := ioutil.ReadFile(target)
- if bytes.Equal(buf, content) {
- logger.Debugf("Certificate was up to date: %s", target)
- return false
- }
-
- err := ioutil.WriteFile(target, content, 0700)
- if err != nil {
- logger.Warnf("Unable to write certificate %s - %s", target, err.Error())
- return false
- } else {
- logger.Infof("Updated certificate file %s", target)
- return true
- }
- }
|