235 lines
6.2 KiB
Go
235 lines
6.2 KiB
Go
package plugin
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"io/ioutil"
|
|
"log"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/caddyserver/caddy/v2"
|
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
|
"github.com/docker/docker/api/types"
|
|
"github.com/docker/docker/api/types/filters"
|
|
"github.com/docker/docker/client"
|
|
"github.com/lucaslorentz/caddy-docker-proxy/plugin/v2/config"
|
|
"github.com/lucaslorentz/caddy-docker-proxy/plugin/v2/docker"
|
|
"github.com/lucaslorentz/caddy-docker-proxy/plugin/v2/generator"
|
|
)
|
|
|
|
// DockerLoader generates caddy files from docker swarm information
|
|
type DockerLoader struct {
|
|
options *config.Options
|
|
initialized bool
|
|
dockerClient docker.Client
|
|
generator *generator.CaddyfileGenerator
|
|
timer *time.Timer
|
|
skipEvents bool
|
|
lastCaddyfile []byte
|
|
lastLogs string
|
|
lastJsonConfig []byte
|
|
updatedServers map[string]struct{}
|
|
}
|
|
|
|
// CreateDockerLoader creates a docker loader
|
|
func CreateDockerLoader(options *config.Options) *DockerLoader {
|
|
return &DockerLoader{
|
|
options: options,
|
|
updatedServers: map[string]struct{}{},
|
|
}
|
|
}
|
|
|
|
// Start docker loader
|
|
func (dockerLoader *DockerLoader) Start() error {
|
|
if !dockerLoader.initialized {
|
|
dockerLoader.initialized = true
|
|
|
|
dockerClient, err := client.NewEnvClient()
|
|
if err != nil {
|
|
log.Printf("[ERROR] Docker connection failed: %s", err)
|
|
return err
|
|
}
|
|
|
|
dockerPing, err := dockerClient.Ping(context.Background())
|
|
if err != nil {
|
|
log.Printf("[ERROR] Docker ping failed: %s", err)
|
|
return err
|
|
}
|
|
|
|
dockerClient.NegotiateAPIVersionPing(dockerPing)
|
|
|
|
wrappedClient := docker.WrapClient(dockerClient)
|
|
|
|
dockerLoader.dockerClient = wrappedClient
|
|
dockerLoader.generator = generator.CreateGenerator(
|
|
wrappedClient,
|
|
docker.CreateUtils(),
|
|
dockerLoader.options,
|
|
)
|
|
|
|
log.Printf("[INFO] CaddyfilePath: %v", dockerLoader.options.CaddyfilePath)
|
|
log.Printf("[INFO] LabelPrefix: %v", dockerLoader.options.LabelPrefix)
|
|
log.Printf("[INFO] PollingInterval: %v", dockerLoader.options.PollingInterval)
|
|
log.Printf("[INFO] ProcessCaddyfile: %v", dockerLoader.options.ProcessCaddyfile)
|
|
log.Printf("[INFO] ProxyServiceTasks: %v", dockerLoader.options.ProxyServiceTasks)
|
|
log.Printf("[INFO] IngressNetworks: %v", dockerLoader.options.IngressNetworks)
|
|
|
|
dockerLoader.timer = time.AfterFunc(0, func() {
|
|
dockerLoader.update()
|
|
})
|
|
|
|
go dockerLoader.monitorEvents()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (dockerLoader *DockerLoader) monitorEvents() {
|
|
args := filters.NewArgs()
|
|
args.Add("scope", "swarm")
|
|
args.Add("scope", "local")
|
|
args.Add("type", "service")
|
|
args.Add("type", "container")
|
|
args.Add("type", "config")
|
|
|
|
eventsChan, errorChan := dockerLoader.dockerClient.Events(context.Background(), types.EventsOptions{
|
|
Filters: args,
|
|
})
|
|
|
|
for {
|
|
select {
|
|
case event := <-eventsChan:
|
|
if dockerLoader.skipEvents {
|
|
continue
|
|
}
|
|
|
|
update := (event.Type == "container" && event.Action == "create") ||
|
|
(event.Type == "container" && event.Action == "start") ||
|
|
(event.Type == "container" && event.Action == "stop") ||
|
|
(event.Type == "container" && event.Action == "die") ||
|
|
(event.Type == "container" && event.Action == "destroy") ||
|
|
(event.Type == "service" && event.Action == "create") ||
|
|
(event.Type == "service" && event.Action == "update") ||
|
|
(event.Type == "service" && event.Action == "remove") ||
|
|
(event.Type == "config" && event.Action == "create") ||
|
|
(event.Type == "config" && event.Action == "remove")
|
|
|
|
if update {
|
|
dockerLoader.skipEvents = true
|
|
dockerLoader.timer.Reset(100 * time.Millisecond)
|
|
}
|
|
case err := <-errorChan:
|
|
log.Println(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (dockerLoader *DockerLoader) update() bool {
|
|
dockerLoader.timer.Reset(dockerLoader.options.PollingInterval)
|
|
dockerLoader.skipEvents = false
|
|
|
|
caddyfile, logs, controlledServers := dockerLoader.generator.GenerateCaddyfile()
|
|
|
|
caddyfileChanged := !bytes.Equal(dockerLoader.lastCaddyfile, caddyfile)
|
|
logsChanged := dockerLoader.lastLogs != logs
|
|
|
|
dockerLoader.lastCaddyfile = caddyfile
|
|
dockerLoader.lastLogs = logs
|
|
|
|
if logsChanged || caddyfileChanged {
|
|
log.Print(logs)
|
|
}
|
|
|
|
if caddyfileChanged {
|
|
log.Printf("[INFO] New Caddyfile:\n%s", caddyfile)
|
|
|
|
adapter := caddyconfig.GetAdapter("caddyfile")
|
|
|
|
configJSON, warn, err := adapter.Adapt(caddyfile, nil)
|
|
|
|
if warn != nil {
|
|
log.Printf("[WARNING] Caddyfile to json warning: %v", warn)
|
|
}
|
|
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed to convert caddyfile into json config: %s", err)
|
|
return false
|
|
}
|
|
|
|
log.Printf("[INFO] New Config JSON:\n%s", configJSON)
|
|
|
|
dockerLoader.lastJsonConfig = configJSON
|
|
dockerLoader.updatedServers = map[string]struct{}{}
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
for _, server := range controlledServers {
|
|
wg.Add(1)
|
|
go dockerLoader.updateServer(&wg, server)
|
|
}
|
|
wg.Wait()
|
|
|
|
return true
|
|
}
|
|
|
|
func (dockerLoader *DockerLoader) updateServer(wg *sync.WaitGroup, server string) {
|
|
defer wg.Done()
|
|
|
|
if _, isUpToDate := dockerLoader.updatedServers[server]; isUpToDate {
|
|
return
|
|
}
|
|
|
|
log.Printf("[INFO] Sending configuration to %v", server)
|
|
|
|
url := "http://" + server + ":2019/load"
|
|
|
|
postBody, err := addAdminListen(dockerLoader.lastJsonConfig, "tcp/"+server+":2019")
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed to add admin listen to %v: %s", server, err)
|
|
return
|
|
}
|
|
|
|
req, err := http.NewRequest("POST", url, bytes.NewBuffer(postBody))
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed to create request to %v: %s", server, err)
|
|
return
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
resp, err := http.DefaultClient.Do(req)
|
|
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed to send configuration to %v: %s", server, err)
|
|
return
|
|
}
|
|
|
|
bodyBytes, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
log.Printf("[ERROR] Failed to read response from %v: %s", server, err)
|
|
return
|
|
}
|
|
|
|
if resp.StatusCode != 200 {
|
|
log.Printf("[ERROR] Error response from %v: %v - %s", server, resp.StatusCode, bodyBytes)
|
|
return
|
|
}
|
|
|
|
dockerLoader.updatedServers[server] = struct{}{}
|
|
|
|
log.Printf("[INFO] Successfully configured %v", server)
|
|
}
|
|
|
|
func addAdminListen(configJSON []byte, listen string) ([]byte, error) {
|
|
config := &caddy.Config{}
|
|
err := json.Unmarshal(configJSON, config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
config.Admin = &caddy.AdminConfig{
|
|
Listen: listen,
|
|
}
|
|
return json.Marshal(config)
|
|
}
|