mirror of
https://github.com/zix99/traefik-lazyload.git
synced 2025-12-24 06:28:31 +01:00
Improvmenets
This commit is contained in:
@@ -11,3 +11,11 @@ starting/stopping containers to save resources.
|
|||||||
|
|
||||||
* `lazyloader=true` -- Add to containers that should be managed
|
* `lazyloader=true` -- Add to containers that should be managed
|
||||||
* `lazyloader.stopdelay=5m` -- Amount of time to wait for idle network traffick before stopping a container (default: 5m)
|
* `lazyloader.stopdelay=5m` -- Amount of time to wait for idle network traffick before stopping a container (default: 5m)
|
||||||
|
|
||||||
|
# Features
|
||||||
|
|
||||||
|
- [ ] Dependencies & groups (eg. shut down DB if all dependent apps are down)
|
||||||
|
|
||||||
|
# License
|
||||||
|
|
||||||
|
GPLv3
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
type ConfigModel struct {
|
type ConfigModel struct {
|
||||||
Listen string // http listen
|
Listen string // http listen
|
||||||
StopAtBoot bool // Stop existing containers at start of app
|
StopAtBoot bool // Stop existing containers at start of app
|
||||||
|
Splash string // Which splash page to serve
|
||||||
|
|
||||||
Labels struct {
|
Labels struct {
|
||||||
Prefix string `mapstructure:"prefix"`
|
Prefix string `mapstructure:"prefix"`
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
listen: :8080
|
listen: :8080
|
||||||
|
|
||||||
stopatboot: false
|
stopatboot: false
|
||||||
|
splash: splash.html
|
||||||
|
|
||||||
labels:
|
labels:
|
||||||
prefix: lazyloader
|
prefix: lazyloader
|
||||||
93
main.go
93
main.go
@@ -9,6 +9,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ const httpAssetPrefix = "/__llassets/"
|
|||||||
var dockerClient *client.Client
|
var dockerClient *client.Client
|
||||||
|
|
||||||
type containerState struct {
|
type containerState struct {
|
||||||
|
Name, ID string
|
||||||
IsRunning bool
|
IsRunning bool
|
||||||
LastWork time.Time
|
LastWork time.Time
|
||||||
StopDelay time.Duration
|
StopDelay time.Duration
|
||||||
@@ -35,9 +37,11 @@ type containerState struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// containerID -> State
|
// containerID -> State
|
||||||
var containerStateCache map[string]*containerState = make(map[string]*containerState)
|
var managedContainers = make(map[string]*containerState)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
|
// Connect to docker
|
||||||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@@ -46,8 +50,22 @@ func main() {
|
|||||||
|
|
||||||
dockerClient = cli
|
dockerClient = cli
|
||||||
|
|
||||||
|
// Test
|
||||||
|
if info, err := cli.Info(context.Background()); err != nil {
|
||||||
|
logrus.Fatal(err)
|
||||||
|
} else {
|
||||||
|
logrus.Infof("Connected docker to %s", info.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if splash, err := httpAssets.ReadFile(path.Join("assets", Config.Splash)); err != nil || len(splash) == 0 {
|
||||||
|
logrus.Fatal("Unable to open splash file %s", Config.Splash)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initial state
|
||||||
if Config.StopAtBoot {
|
if Config.StopAtBoot {
|
||||||
stopAllLazyContainers()
|
stopAllLazyContainers()
|
||||||
|
} else {
|
||||||
|
//TODO: Inventory currently running containers
|
||||||
}
|
}
|
||||||
|
|
||||||
go watchForInactive()
|
go watchForInactive()
|
||||||
@@ -60,22 +78,29 @@ func main() {
|
|||||||
http.ListenAndServe(Config.Listen, nil)
|
http.ListenAndServe(Config.Listen, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func stopAllLazyContainers() {
|
func stopAllLazyContainers() error {
|
||||||
filter := filters.NewArgs()
|
filter := filters.NewArgs()
|
||||||
filter.Add("label", "lazyloader")
|
filter.Add("label", "lazyloader")
|
||||||
|
|
||||||
containers, _ := dockerClient.ContainerList(context.Background(), types.ContainerListOptions{Filters: filter, All: true})
|
containers, err := dockerClient.ContainerList(context.Background(), types.ContainerListOptions{Filters: filter, All: true})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, _ := context.WithTimeout(context.Background(), 1*time.Minute)
|
||||||
|
|
||||||
for _, c := range containers {
|
for _, c := range containers {
|
||||||
logrus.Infof("Stopping %s: %s", c.ID[:8], c.Names[0])
|
logrus.Infof("Stopping %s: %s", c.ID[:8], c.Names[0])
|
||||||
dockerClient.ContainerStop(context.Background(), c.ID, container.StopOptions{})
|
dockerClient.ContainerStop(ctx, c.ID, container.StopOptions{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func watchForInactive() {
|
func watchForInactive() {
|
||||||
// TODO: Thread safety
|
// TODO: Thread safety
|
||||||
for {
|
for {
|
||||||
for cid, ct := range containerStateCache {
|
for cid, ct := range managedContainers {
|
||||||
if !ct.IsRunning {
|
if !ct.IsRunning {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -109,12 +134,12 @@ func watchForInactive() {
|
|||||||
|
|
||||||
// No network activity for a while, stop?
|
// No network activity for a while, stop?
|
||||||
if time.Now().After(ct.LastWork.Add(ct.StopDelay)) {
|
if time.Now().After(ct.LastWork.Add(ct.StopDelay)) {
|
||||||
logrus.Infof("Stopping idle container %s...", short(cid))
|
logrus.Infof("Stopping idle container %s...", ct.Name)
|
||||||
err := dockerClient.ContainerStop(context.Background(), cid, container.StopOptions{})
|
err := dockerClient.ContainerStop(context.Background(), cid, container.StopOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Warnf("Error stopping container: %s", err)
|
logrus.Warnf("Error stopping container: %s", err)
|
||||||
} else {
|
} else {
|
||||||
delete(containerStateCache, cid)
|
delete(managedContainers, cid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -131,34 +156,29 @@ func ContainerHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ct, _ := findContainerWithRoute(r.Context(), host) // TODO: Use cache rather than query
|
ct, _ := findContainerByHostname(r.Context(), host)
|
||||||
if ct != nil {
|
if ct != nil {
|
||||||
// TODO: Send response before querying anything about the container (the slow bit)
|
// TODO: Send response before querying anything about the container (the slow bit)
|
||||||
splash, _ := httpAssets.Open("assets/splash.html")
|
splash, _ := httpAssets.Open(path.Join("assets", Config.Splash))
|
||||||
|
w.WriteHeader(http.StatusAccepted)
|
||||||
io.Copy(w, splash)
|
io.Copy(w, splash)
|
||||||
|
|
||||||
|
// Look to start the container
|
||||||
|
state := getOrCreateState(ct.ID)
|
||||||
logrus.Infof("Found container %s for host %s, checking state...", containerShort(ct), host)
|
logrus.Infof("Found container %s for host %s, checking state...", containerShort(ct), host)
|
||||||
state := getOrCreateCache(ct.ID)
|
|
||||||
|
|
||||||
if !state.IsRunning {
|
if !state.IsRunning { // cache doesn't think it's running
|
||||||
details, _ := dockerClient.ContainerInspect(r.Context(), ct.ID)
|
if ct.State != "running" {
|
||||||
|
logrus.Infof("Container %s not running (is %s), starting...", state.Name, ct.State)
|
||||||
if !details.State.Running {
|
go dockerClient.ContainerStart(context.Background(), ct.ID, types.ContainerStartOptions{}) // TODO: Check error
|
||||||
logrus.Infof("Container %s not running, starting...", containerShort(ct))
|
|
||||||
dockerClient.ContainerStart(r.Context(), ct.ID, types.ContainerStartOptions{})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
state.IsRunning = true
|
state.IsRunning = true
|
||||||
|
state.Name = containerShort(ct)
|
||||||
|
state.ID = ct.ID
|
||||||
state.LastWork = time.Now()
|
state.LastWork = time.Now()
|
||||||
|
parseContainerSettings(state, ct)
|
||||||
var stopErr error
|
} // TODO: What if container crahsed but we think it's started?
|
||||||
stopDelay, _ := labelOrDefault(ct, "stopdelay", "10s")
|
|
||||||
state.StopDelay, stopErr = time.ParseDuration(stopDelay)
|
|
||||||
if stopErr != nil {
|
|
||||||
state.StopDelay = 30 * time.Second
|
|
||||||
logrus.Warnf("Unable to parse stopdelay of %s, defaulting to %s", stopDelay, state.StopDelay.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
logrus.Warnf("Unable to find container for host %s", host)
|
logrus.Warnf("Unable to find container for host %s", host)
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
@@ -166,16 +186,28 @@ func ContainerHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getOrCreateCache(cid string) (ret *containerState) {
|
func getOrCreateState(cid string) (ret *containerState) {
|
||||||
var ok bool
|
var ok bool
|
||||||
if ret, ok = containerStateCache[cid]; !ok {
|
if ret, ok = managedContainers[cid]; !ok {
|
||||||
ret = &containerState{}
|
ret = &containerState{}
|
||||||
containerStateCache[cid] = ret
|
managedContainers[cid] = ret
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func findContainerWithRoute(ctx context.Context, route string) (*types.Container, error) {
|
func parseContainerSettings(target *containerState, ct *types.Container) {
|
||||||
|
{ // Parse stop delay
|
||||||
|
var stopErr error
|
||||||
|
stopDelay, _ := labelOrDefault(ct, "stopdelay", "10s")
|
||||||
|
target.StopDelay, stopErr = time.ParseDuration(stopDelay)
|
||||||
|
if stopErr != nil {
|
||||||
|
target.StopDelay = 30 * time.Second
|
||||||
|
logrus.Warnf("Unable to parse stopdelay of %s, defaulting to %s", stopDelay, target.StopDelay.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func findContainerByHostname(ctx context.Context, hostname string) (*types.Container, error) {
|
||||||
containers, err := findAllLazyloadContainers(ctx, true)
|
containers, err := findAllLazyloadContainers(ctx, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -183,7 +215,7 @@ func findContainerWithRoute(ctx context.Context, route string) (*types.Container
|
|||||||
|
|
||||||
for _, c := range containers {
|
for _, c := range containers {
|
||||||
for k, v := range c.Labels {
|
for k, v := range c.Labels {
|
||||||
if strings.Contains(k, "traefik.http.routers.") && strings.Contains(v, route) { // TODO: More complex, and self-ignore
|
if strings.Contains(k, "traefik.http.routers.") && strings.Contains(v, hostname) { // TODO: More complex, and self-ignore
|
||||||
return &c, nil
|
return &c, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,6 +224,7 @@ func findContainerWithRoute(ctx context.Context, route string) (*types.Container
|
|||||||
return nil, errors.New("not found")
|
return nil, errors.New("not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Finds all containers on node that are labeled with lazyloader config
|
||||||
func findAllLazyloadContainers(ctx context.Context, includeStopped bool) ([]types.Container, error) {
|
func findAllLazyloadContainers(ctx context.Context, includeStopped bool) ([]types.Container, error) {
|
||||||
filters := filters.NewArgs()
|
filters := filters.NewArgs()
|
||||||
filters.Add("label", Config.Labels.Prefix)
|
filters.Add("label", Config.Labels.Prefix)
|
||||||
|
|||||||
Reference in New Issue
Block a user