Handle signals, code cleanup, easier labels

This commit is contained in:
Christopher LaPointe
2023-05-26 19:52:17 -04:00
parent af94d29dd3
commit ebb45b55c3
10 changed files with 123 additions and 124 deletions

View File

@@ -1,5 +1,5 @@
README.md README.md
docker-compose.yml *compose.yml
Dockerfile Dockerfile
.gitignore .gitignore
traefik-lazyload traefik-lazyload

4
.gitignore vendored
View File

@@ -1 +1,5 @@
# Binary
traefik-lazyload traefik-lazyload
# Various testing/compose files
*compose.yml

View File

@@ -14,10 +14,8 @@ var httpAssets embed.FS
const httpAssetPrefix = "/__llassets/" const httpAssetPrefix = "/__llassets/"
type SplashModel struct { type SplashModel struct {
*service.ContainerState
Name string Name string
CID string
WaitForCode int
WaitForPath string
} }
var splashTemplate = template.Must(template.ParseFS(httpAssets, path.Join("assets", config.Model.Splash))) var splashTemplate = template.Must(template.ParseFS(httpAssets, path.Join("assets", config.Model.Splash)))

View File

@@ -23,13 +23,13 @@
</div> </div>
<div class="message"> <div class="message">
<h2>Starting {{.Name}}</h2> <h2>Starting {{.Name}}</h2>
<h3>{{.CID}}</h3> <h3>{{.Name}}</h3>
</div> </div>
</div> </div>
<script> <script>
async function testForOk(url) { async function testForOk(url) {
const response = await fetch(url, { const response = await fetch(url, {
method: "HEAD", method: "{{.WaitForMethod}}",
}); });
console.log(`Got ${response.status}`); console.log(`Got ${response.status}`);
return response.status === {{.WaitForCode}}; return response.status === {{.WaitForCode}};

View File

@@ -1,57 +0,0 @@
version: '3.5'
services:
# reverse-proxy:
# image: traefik:v2.4
# command:
# - --api.insecure
# - --providers.docker
# - --providers.docker.defaultRule=Host(`{{.Name}}.d0.lan`)
# - --entryPoints.web.address=:85
# - --entryPoints.web.forwardedHeaders.insecure
# - --providers.docker.exposedByDefault=false
# - --providers.docker.constraints=Label(`my.zone`,`zone1`)
# restart: always
# ports:
# - "85:85" # The HTTP port
# - "8086:8080" # The Web UI (enabled by --api)
# volumes:
# - /var/run/docker.sock:/var/run/docker.sock # So that Traefik can listen to the Docker events
monitor:
build: .
labels:
- traefik.enable=true
- "traefik.http.routers.lazyload.priority=-100"
- "traefik.http.routers.lazyload.rule=Host(`whoami.d.lan`, `whoami2.d.lan`, `pdf.d.lan`, `noexist.d.lan`, `lazyloader.d.lan`)"
environment:
TLL_STOPATBOOT: true
TLL_STATUSHOST: lazyloader.d.lan
networks:
- traefik-bridge
volumes:
- /var/run/docker.sock:/var/run/docker.sock
whoami:
image: containous/whoami
networks:
- traefik-bridge
labels:
- traefik.enable=true
- "traefik.http.routers.lazywhoami.rule=Host(`whoami.d.lan`)"
- lazyloader=true
whoami2:
image: containous/whoami
networks:
- traefik-bridge
labels:
- traefik.enable=true
- "traefik.http.routers.lazywhoami2.rule=Host(`whoami2.d.lan`)"
- lazyloader=true
- lazyloader.stopdelay=1m
networks:
traefik-bridge:
external: true
name: traefik-bridge

30
main.go
View File

@@ -1,11 +1,14 @@
package main package main
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/fs" "io/fs"
"net/http" "net/http"
"os"
"os/signal"
"runtime" "runtime"
"traefik-lazyload/pkg/config" "traefik-lazyload/pkg/config"
"traefik-lazyload/pkg/service" "traefik-lazyload/pkg/service"
@@ -44,14 +47,31 @@ func main() {
// Set up http server // Set up http server
subFs, _ := fs.Sub(httpAssets, "assets") subFs, _ := fs.Sub(httpAssets, "assets")
http.Handle(httpAssetPrefix, http.StripPrefix(httpAssetPrefix, http.FileServer(http.FS(subFs)))) router := http.NewServeMux()
http.HandleFunc("/", ContainerHandler) router.Handle(httpAssetPrefix, http.StripPrefix(httpAssetPrefix, http.FileServer(http.FS(subFs))))
router.HandleFunc("/", ContainerHandler)
srv := &http.Server{
Addr: config.Model.Listen,
Handler: router,
}
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt)
go func() {
<-sigChan
logrus.Info("Shutting down...")
srv.Shutdown(context.Background())
}()
logrus.Infof("Listening on %s...", config.Model.Listen) logrus.Infof("Listening on %s...", config.Model.Listen)
if config.Model.StatusHost != "" { if config.Model.StatusHost != "" {
logrus.Infof("Status host set to %s", config.Model.StatusHost) logrus.Infof("Status host set to %s", config.Model.StatusHost)
} }
http.ListenAndServe(config.Model.Listen, nil) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logrus.Fatal(err)
}
} }
func ContainerHandler(w http.ResponseWriter, r *http.Request) { func ContainerHandler(w http.ResponseWriter, r *http.Request) {
@@ -78,9 +98,7 @@ func ContainerHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusAccepted) w.WriteHeader(http.StatusAccepted)
renderErr := splashTemplate.Execute(w, SplashModel{ renderErr := splashTemplate.Execute(w, SplashModel{
Name: host, Name: host,
CID: sOpts.ContainerName, ContainerState: sOpts,
WaitForCode: sOpts.WaitForCode,
WaitForPath: sOpts.WaitForPath,
}) })
if renderErr != nil { if renderErr != nil {
logrus.Error(renderErr) logrus.Error(renderErr)

View File

@@ -1,19 +1,19 @@
package service package service
import ( import (
"strconv"
"strings" "strings"
"time" "time"
"traefik-lazyload/pkg/config" "traefik-lazyload/pkg/config"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/sirupsen/logrus"
) )
type containerSettings struct { type containerSettings struct {
stopDelay time.Duration stopDelay time.Duration
waitForCode int waitForCode int
waitForPath string waitForPath string
waitForMethod string
needs []string
} }
type ContainerState struct { type ContainerState struct {
@@ -34,26 +34,11 @@ func newStateFromContainer(ct *types.Container) *ContainerState {
} }
func extractContainerLabels(ct *types.Container) (target containerSettings) { func extractContainerLabels(ct *types.Container) (target containerSettings) {
{ // Parse stop delay target.stopDelay, _ = labelOrDefaultDuration(ct, "stopdelay", config.Model.StopDelay)
stopDelay, _ := labelOrDefault(ct, "stopdelay", config.Model.StopDelay.String()) target.waitForCode, _ = labelOrDefaultInt(ct, "waitforcode", 200)
if dur, stopErr := time.ParseDuration(stopDelay); stopErr != nil {
target.stopDelay = config.Model.StopDelay
logrus.Warnf("Unable to parse stopdelay for %s of %s, defaulting to %s", containerShort(ct), stopDelay, target.stopDelay.String())
} else {
target.stopDelay = dur
}
}
{ // WaitForCode
codeStr, _ := labelOrDefault(ct, "waitforcode", "200")
if code, err := strconv.Atoi(codeStr); err != nil {
target.waitForCode = 200
logrus.Warnf("Unable to parse WaitForCode of %s, defaulting to %d", containerShort(ct), target.waitForCode)
} else {
target.waitForCode = code
}
}
target.waitForPath, _ = labelOrDefault(ct, "waitforpath", "/") target.waitForPath, _ = labelOrDefault(ct, "waitforpath", "/")
target.waitForMethod, _ = labelOrDefault(ct, "waitformethod", "HEAD")
target.needs, _ = labelOrDefaultArr(ct, "needs")
return return
} }
@@ -85,6 +70,19 @@ func (s *containerSettings) StopDelay() string {
return s.stopDelay.String() return s.stopDelay.String()
} }
func (s *ContainerState) WaitForCode() int {
return s.waitForCode
}
func (s *ContainerState) WaitForPath() string {
return s.waitForPath
}
func (s *ContainerState) WaitForMethod() string {
return s.waitForMethod
}
// Wrapper for container results that opaques and adds some methods to that data
type ContainerWrapper struct { type ContainerWrapper struct {
types.Container types.Container
} }

53
pkg/service/labels.go Normal file
View File

@@ -0,0 +1,53 @@
package service
import (
"strconv"
"strings"
"time"
"traefik-lazyload/pkg/config"
"github.com/docker/docker/api/types"
"github.com/sirupsen/logrus"
)
func labelOrDefault(ct *types.Container, sublabel, dflt string) (string, bool) {
if val, ok := ct.Labels[config.SubLabel(sublabel)]; ok {
return val, true
}
return dflt, false
}
func labelOrDefaultArr(ct *types.Container, sublabel string) ([]string, bool) {
if val, ok := ct.Labels[config.SubLabel(sublabel)]; ok {
return strings.Split(val, ","), true
}
return []string{}, false
}
func labelOrDefaultInt(ct *types.Container, sublabel string, dflt int) (int, bool) {
s, ok := labelOrDefault(ct, sublabel, "")
if !ok {
return dflt, false
}
if val, err := strconv.Atoi(s); err != nil {
logrus.Warnf("Unable to parse %s on %s: %v. Using default of %d", sublabel, containerShort(ct), err, dflt)
return dflt, false
} else {
return val, true
}
}
func labelOrDefaultDuration(ct *types.Container, sublabel string, dflt time.Duration) (time.Duration, bool) {
s, ok := labelOrDefault(ct, sublabel, "")
if !ok {
return dflt, false
}
if val, err := time.ParseDuration(s); err != nil {
logrus.Warnf("Unable to parse %s on %s: %v. Using default of %s", sublabel, containerShort(ct), err, dflt.String())
return dflt, false
} else {
return val, true
}
}

View File

@@ -55,13 +55,7 @@ func (s *Core) Close() error {
return s.client.Close() return s.client.Close()
} }
type StartResult struct { func (s *Core) StartHost(hostname string) (*ContainerState, error) {
WaitForCode int
WaitForPath string
ContainerName string
}
func (s *Core) StartHost(hostname string) (*StartResult, error) {
s.mux.Lock() s.mux.Lock()
defer s.mux.Unlock() defer s.mux.Unlock()
@@ -75,11 +69,7 @@ func (s *Core) StartHost(hostname string) (*StartResult, error) {
if ets, exists := s.active[ct.ID]; exists { if ets, exists := s.active[ct.ID]; exists {
logrus.Debugf("Asked to start host, but we already think it's started: %s", ets.name) logrus.Debugf("Asked to start host, but we already think it's started: %s", ets.name)
return &StartResult{ return ets, nil
WaitForCode: ets.waitForCode,
WaitForPath: ets.waitForPath,
ContainerName: containerShort(ct),
}, nil
} }
go s.startContainer(ctx, ct) go s.startContainer(ctx, ct)
@@ -88,10 +78,7 @@ func (s *Core) StartHost(hostname string) (*StartResult, error) {
ets := newStateFromContainer(ct) ets := newStateFromContainer(ct)
s.active[ct.ID] = ets s.active[ct.ID] = ets
return &StartResult{ return ets, nil
WaitForCode: ets.waitForCode,
WaitForPath: ets.waitForPath,
}, nil
} }
func (s *Core) StopAll() { func (s *Core) StopAll() {
@@ -229,6 +216,15 @@ func (s *Core) checkContainerForInactivity(ctx context.Context, cid string, ct *
return false, nil return false, nil
} }
func (s *Core) findContainersByDepProvider(ctx context.Context, name string) ([]types.Container, error) {
filters := filters.NewArgs()
filters.Add("label", config.SubLabel("providers")+"="+name)
return s.client.ContainerList(ctx, types.ContainerListOptions{
Filters: filters,
All: true,
})
}
func (s *Core) findContainerByHostname(ctx context.Context, hostname string) (*types.Container, error) { func (s *Core) findContainerByHostname(ctx context.Context, hostname string) (*types.Container, error) {
containers, err := s.findAllLazyloadContainers(ctx, true) containers, err := s.findAllLazyloadContainers(ctx, true)
if err != nil { if err != nil {

View File

@@ -3,7 +3,6 @@ package service
import ( import (
"fmt" "fmt"
"strings" "strings"
"traefik-lazyload/pkg/config"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
) )
@@ -16,13 +15,6 @@ func sumNetworkBytes(networks map[string]types.NetworkStats) (recv int64, send i
return return
} }
func labelOrDefault(ct *types.Container, sublabel, dflt string) (string, bool) {
if val, ok := ct.Labels[config.SubLabel(sublabel)]; ok {
return val, true
}
return dflt, false
}
func shortId(id string) string { func shortId(id string) string {
const SLEN = 8 const SLEN = 8
if len(id) <= SLEN { if len(id) <= SLEN {
@@ -42,10 +34,7 @@ func containerShort(c *types.Container) string {
} }
func trimRootPath(s string) string { func trimRootPath(s string) string {
if strings.HasPrefix(s, "/") { return strings.TrimPrefix(s, "/")
return s[1:]
}
return s
} }
func isRunning(c *types.Container) bool { func isRunning(c *types.Container) bool {