diff --git a/README.md b/README.md index 63e5c61..2da8aaa 100644 --- a/README.md +++ b/README.md @@ -112,14 +112,10 @@ labelprefix: lazyloader ### Dependencies -* `lazyloader.needs=a,b,c` -- List of dependencies a container needs (will be started before starting the container) +* `lazyloader.needs=a,b,c` -- List of dependencies a container needs (will be started before starting the container). Can only be specified on a `lazyloader=true` container * `lazyloader.provides=a` -- What dependency name a container provides (Not necessarily a `lazyloader` container) * `lazyloader.provides.delay=5s` -- Delay starting other containers for this duration -# Features - -- [ ] Dependencies & groups (eg. shut down DB if all dependent apps are down) - # License GPLv3 diff --git a/config.yaml b/config.yaml index f510ed3..5020f57 100644 --- a/config.yaml +++ b/config.yaml @@ -17,6 +17,9 @@ splash: splash.html stopdelay: 5m # How long to wait before stopping container pollfreq: 10s # How often to check +# Default operation timeout (eg. starting and stopping a container) +timeout: 30s + # This will be the label-prefix to look at settings on a container # usually won't need to change (only if running multiple instances) labelprefix: lazyloader diff --git a/main.go b/main.go index 80023db..220afc0 100644 --- a/main.go +++ b/main.go @@ -18,8 +18,10 @@ import ( "github.com/sirupsen/logrus" ) -var core *service.Core -var discovery *containers.Discovery +type controller struct { + core *service.Core + discovery *containers.Discovery +} func mustCreateDockerClient() *client.Client { cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) @@ -37,10 +39,10 @@ func main() { } dockerClient := mustCreateDockerClient() - discovery = containers.NewDiscovery(dockerClient) + discovery := containers.NewDiscovery(dockerClient) var err error - core, err = service.New(dockerClient, discovery, config.Model.PollFreq) + core, err := service.New(dockerClient, discovery, config.Model.PollFreq) if err != nil { logrus.Fatal(err) } @@ -50,11 +52,16 @@ func main() { core.StopAll() } + controller := controller{ + core, + discovery, + } + // Set up http server subFs, _ := fs.Sub(httpAssets, "assets") router := http.NewServeMux() router.Handle(httpAssetPrefix, http.StripPrefix(httpAssetPrefix, http.FileServer(http.FS(subFs)))) - router.HandleFunc("/", ContainerHandler) + router.HandleFunc("/", controller.ContainerHandler) srv := &http.Server{ Addr: config.Model.Listen, @@ -79,7 +86,7 @@ func main() { } } -func ContainerHandler(w http.ResponseWriter, r *http.Request) { +func (s *controller) ContainerHandler(w http.ResponseWriter, r *http.Request) { host := r.Host if host == "" { w.WriteHeader(http.StatusNotFound) @@ -87,11 +94,11 @@ func ContainerHandler(w http.ResponseWriter, r *http.Request) { return } if host == config.Model.StatusHost && config.Model.StatusHost != "" { - StatusHandler(w, r) + s.StatusHandler(w, r) return } - if sOpts, err := core.StartHost(host); err != nil { + if sOpts, err := s.core.StartHost(host); err != nil { if errors.Is(err, containers.ErrNotFound) { w.WriteHeader(http.StatusNotFound) io.WriteString(w, "not found") @@ -111,17 +118,17 @@ func ContainerHandler(w http.ResponseWriter, r *http.Request) { } } -func StatusHandler(w http.ResponseWriter, r *http.Request) { +func (s *controller) StatusHandler(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/": var stats runtime.MemStats runtime.ReadMemStats(&stats) - qualifying, _ := discovery.QualifyingContainers(r.Context()) - providers, _ := discovery.ProviderContainers(r.Context()) + qualifying, _ := s.discovery.QualifyingContainers(r.Context()) + providers, _ := s.discovery.ProviderContainers(r.Context()) statusPageTemplate.Execute(w, StatusPageModel{ - Active: core.ActiveContainers(), + Active: s.core.ActiveContainers(), Qualifying: qualifying, Providers: providers, RuntimeMetrics: fmt.Sprintf("Heap=%d, InUse=%d, Total=%d, Sys=%d, NumGC=%d", stats.HeapAlloc, stats.HeapInuse, stats.TotalAlloc, stats.Sys, stats.NumGC), diff --git a/pkg/config/config.go b/pkg/config/config.go index 32fb862..679ac8c 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -19,6 +19,7 @@ type ConfigModel struct { StopDelay time.Duration // Amount of time to wait before stopping a container PollFreq time.Duration // How often to check for changes + Timeout time.Duration // Default operation timeout (eg. starting/stopping a container) Verbose bool // Debug-level logging diff --git a/pkg/service/service.go b/pkg/service/service.go index c143ea7..b9ea6ab 100644 --- a/pkg/service/service.go +++ b/pkg/service/service.go @@ -7,6 +7,7 @@ import ( "sort" "sync" "time" + "traefik-lazyload/pkg/config" "traefik-lazyload/pkg/containers" "github.com/docker/docker/api/types" @@ -59,16 +60,18 @@ func (s *Core) StartHost(hostname string) (*ContainerState, error) { s.mux.Lock() defer s.mux.Unlock() - ctx := context.Background() + ctx, cancel := context.WithTimeout(context.Background(), config.Model.Timeout) ct, err := s.discovery.FindContainerByHostname(ctx, hostname) if err != nil { logrus.Warnf("Unable to find container for host %s: %s", hostname, err) + cancel() return nil, err } if ets, exists := s.active[ct.ID]; exists { logrus.Debugf("Asked to start host, but we already think it's started: %s", ets.name) + cancel() return ets, nil } @@ -80,6 +83,7 @@ func (s *Core) StartHost(hostname string) (*ContainerState, error) { go func() { defer func() { + cancel() s.mux.Lock() ets.pinned = false ets.lastActivity = time.Now() @@ -102,8 +106,11 @@ func (s *Core) StopAll() { logrus.Info("Stopping all containers...") for cid, ct := range s.active { logrus.Infof("Stopping %s...", ct.name) - s.client.ContainerStop(ctx, cid, container.StopOptions{}) - delete(s.active, cid) + if err := s.client.ContainerStop(ctx, cid, container.StopOptions{}); err != nil { + logrus.Warnf("Error stopping %s: %v", ct.name, err) + } else { + delete(s.active, cid) + } } } @@ -136,20 +143,24 @@ func (s *Core) startContainerSync(ctx context.Context, ct *containers.Wrapper) e return nil } -func (s *Core) startDependencyFor(ctx context.Context, needs []string, forContainer string) { +func (s *Core) startDependencyFor(ctx context.Context, needs []string, forContainer string) error { for _, dep := range needs { providers, err := s.discovery.FindDepProvider(ctx, dep) if err != nil { logrus.Errorf("Error finding dependency provider for %s: %v", dep, err) + return err } else if len(providers) == 0 { logrus.Warnf("Unable to find any container that provides %s for %s", dep, forContainer) + return ErrProviderNotFound } else { for _, provider := range providers { if !provider.IsRunning() { logrus.Infof("Starting dependency for %s: %s", forContainer, provider.NameID()) - s.startContainerSync(ctx, &provider) + if err := s.startContainerSync(ctx, &provider); err != nil { + return err + } delay, _ := provider.ConfigDuration("provides.delay", 2*time.Second) logrus.Debugf("Delaying %s to start %s", delay.String(), dep) @@ -158,10 +169,13 @@ func (s *Core) startDependencyFor(ctx context.Context, needs []string, forContai } } } + + return nil } -func (s *Core) stopDependenciesFor(ctx context.Context, cid string, cts *ContainerState) { +func (s *Core) stopDependenciesFor(ctx context.Context, cid string, cts *ContainerState) []error { // Look at our needs, and see if anything else needs them; if not, shut down + var errs []error deps := make(map[string]bool) // dep -> needed for _, dep := range cts.needs { @@ -182,6 +196,7 @@ func (s *Core) stopDependenciesFor(ctx context.Context, cid string, cts *Contain containers, err := s.discovery.FindDepProvider(ctx, dep) if err != nil { logrus.Errorf("Unable to find dependency provider containers for %s: %v", dep, err) + errs = append(errs, err) } else if len(containers) == 0 { logrus.Warnf("Unable to find any containers for dependency %s", dep) } else { @@ -195,6 +210,7 @@ func (s *Core) stopDependenciesFor(ctx context.Context, cid string, cts *Contain } } + return errs } // Ticker loop that will check internal state against docker state (Call Poll) @@ -216,7 +232,7 @@ func (s *Core) pollThread(rate time.Duration) { // stopping idle containers // Will normally happen in the background with the pollThread func (s *Core) Poll() { - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), config.Model.Timeout) defer cancel() s.checkForNewContainersSync(ctx)