From f9f5e66553bf084c899be38c02f47a834855379d Mon Sep 17 00:00:00 2001 From: Christopher LaPointe Date: Tue, 30 May 2023 21:57:01 -0400 Subject: [PATCH] Code refactor, separate discovery from manager --- assets.go | 5 +- main.go | 17 ++- pkg/containers/discovery.go | 78 +++++++++++++ pkg/{service => containers}/errors.go | 2 +- pkg/containers/util.go | 10 ++ pkg/containers/wrapper.go | 120 ++++++++++++++++++++ pkg/service/container.go | 57 ++-------- pkg/service/labels.go | 53 --------- pkg/service/service.go | 157 +++++++------------------- pkg/service/util.go | 38 ------- 10 files changed, 278 insertions(+), 259 deletions(-) create mode 100644 pkg/containers/discovery.go rename pkg/{service => containers}/errors.go (77%) create mode 100644 pkg/containers/util.go create mode 100644 pkg/containers/wrapper.go delete mode 100644 pkg/service/labels.go diff --git a/assets.go b/assets.go index d271b29..0a150f8 100644 --- a/assets.go +++ b/assets.go @@ -5,6 +5,7 @@ import ( "path" "text/template" "traefik-lazyload/pkg/config" + "traefik-lazyload/pkg/containers" "traefik-lazyload/pkg/service" ) @@ -22,8 +23,8 @@ var splashTemplate = template.Must(template.ParseFS(httpAssets, path.Join("asset type StatusPageModel struct { Active []*service.ContainerState - Qualifying []service.ContainerWrapper - Providers []service.ContainerWrapper + Qualifying []containers.Wrapper + Providers []containers.Wrapper RuntimeMetrics string } diff --git a/main.go b/main.go index 1f71a74..80023db 100644 --- a/main.go +++ b/main.go @@ -11,6 +11,7 @@ import ( "os/signal" "runtime" "traefik-lazyload/pkg/config" + "traefik-lazyload/pkg/containers" "traefik-lazyload/pkg/service" "github.com/docker/docker/client" @@ -18,6 +19,7 @@ import ( ) var core *service.Core +var discovery *containers.Discovery func mustCreateDockerClient() *client.Client { cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) @@ -34,8 +36,11 @@ func main() { logrus.Debug("Verbose is on") } + dockerClient := mustCreateDockerClient() + discovery = containers.NewDiscovery(dockerClient) + var err error - core, err = service.New(mustCreateDockerClient(), config.Model.PollFreq) + core, err = service.New(dockerClient, discovery, config.Model.PollFreq) if err != nil { logrus.Fatal(err) } @@ -87,7 +92,7 @@ func ContainerHandler(w http.ResponseWriter, r *http.Request) { } if sOpts, err := core.StartHost(host); err != nil { - if errors.Is(err, service.ErrNotFound) { + if errors.Is(err, containers.ErrNotFound) { w.WriteHeader(http.StatusNotFound) io.WriteString(w, "not found") } else { @@ -111,10 +116,14 @@ func StatusHandler(w http.ResponseWriter, r *http.Request) { case "/": var stats runtime.MemStats runtime.ReadMemStats(&stats) + + qualifying, _ := discovery.QualifyingContainers(r.Context()) + providers, _ := discovery.ProviderContainers(r.Context()) + statusPageTemplate.Execute(w, StatusPageModel{ Active: core.ActiveContainers(), - Qualifying: core.QualifyingContainers(r.Context()), - Providers: core.ProviderContainers(r.Context()), + 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), }) default: diff --git a/pkg/containers/discovery.go b/pkg/containers/discovery.go new file mode 100644 index 0000000..5158b8c --- /dev/null +++ b/pkg/containers/discovery.go @@ -0,0 +1,78 @@ +package containers + +import ( + "context" + "strings" + "traefik-lazyload/pkg/config" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/client" +) + +type Discovery struct { + client *client.Client +} + +func NewDiscovery(client *client.Client) *Discovery { + return &Discovery{client} +} + +// Return all containers that qualify to be load-managed (eg. have the tag) +func (s *Discovery) QualifyingContainers(ctx context.Context) ([]Wrapper, error) { + return s.FindAllLazyload(ctx, true) +} + +func (s *Discovery) ProviderContainers(ctx context.Context) ([]Wrapper, error) { + filters := filters.NewArgs() + filters.Add("label", config.SubLabel("provides")) + + return wrapListResult(s.client.ContainerList(ctx, types.ContainerListOptions{ + Filters: filters, + All: true, + })) +} + +func (s *Discovery) FindAllLazyload(ctx context.Context, includeStopped bool) ([]Wrapper, error) { + filters := filters.NewArgs() + filters.Add("label", config.Model.LabelPrefix) + + return wrapListResult(s.client.ContainerList(ctx, types.ContainerListOptions{ + All: includeStopped, + Filters: filters, + })) +} + +func (s *Discovery) FindContainerByHostname(ctx context.Context, hostname string) (*Wrapper, error) { + containers, err := s.FindAllLazyload(ctx, true) + if err != nil { + return nil, err + } + + for _, c := range containers { + if hostStr, ok := c.Config("hosts"); ok { + hosts := strings.Split(hostStr, ",") + if strSliceContains(hosts, hostname) { + return &c, nil + } + } else { + // If not defined explicitely, infer from traefik route + for k, v := range c.Labels { + if strings.Contains(k, "traefik.http.routers.") && strings.Contains(v, hostname) { // TODO: More complex + return &c, nil + } + } + } + } + + return nil, ErrNotFound +} + +func (s *Discovery) FindDepProvider(ctx context.Context, name string) ([]Wrapper, error) { + filters := filters.NewArgs() + filters.Add("label", config.SubLabel("provides")+"="+name) + return wrapListResult(s.client.ContainerList(ctx, types.ContainerListOptions{ + Filters: filters, + All: true, + })) +} diff --git a/pkg/service/errors.go b/pkg/containers/errors.go similarity index 77% rename from pkg/service/errors.go rename to pkg/containers/errors.go index dad5aa2..5b32f34 100644 --- a/pkg/service/errors.go +++ b/pkg/containers/errors.go @@ -1,4 +1,4 @@ -package service +package containers import "errors" diff --git a/pkg/containers/util.go b/pkg/containers/util.go new file mode 100644 index 0000000..c68b5df --- /dev/null +++ b/pkg/containers/util.go @@ -0,0 +1,10 @@ +package containers + +func strSliceContains(slice []string, s string) bool { + for _, item := range slice { + if item == s { + return true + } + } + return false +} diff --git a/pkg/containers/wrapper.go b/pkg/containers/wrapper.go new file mode 100644 index 0000000..862959b --- /dev/null +++ b/pkg/containers/wrapper.go @@ -0,0 +1,120 @@ +package containers + +import ( + "fmt" + "sort" + "strconv" + "strings" + "time" + "traefik-lazyload/pkg/config" + + "github.com/docker/docker/api/types" + "github.com/sirupsen/logrus" +) + +// Wrapper for container results that opaques and adds some methods to that data +type Wrapper struct { + types.Container +} + +// Human-consumable name + ID +func (s *Wrapper) NameID() string { + var name string + if len(s.Names) > 0 { + name = strings.TrimPrefix(s.Names[0], "/") + } else { + name = s.Image + } + return fmt.Sprintf("%s (%s)", name, s.ShortId()) +} + +// char-len capped ID +func (s *Wrapper) ShortId() string { + const SLEN = 8 + if len(s.ID) <= SLEN { + return s.ID + } + return s.ID[:SLEN] +} + +// Returns config labels with the prefix trimmed +func (s *Wrapper) ConfigLabels() map[string]string { + var matchString = config.Model.LabelPrefix + "." + + ret := make(map[string]string) + for k, v := range s.Labels { + if strings.HasPrefix(k, matchString) { + ret[k[len(matchString):]] = v + } + } + + return ret +} + +func (s *Wrapper) Config(sublabel string) (string, bool) { + ret, ok := s.Labels[config.SubLabel(sublabel)] + return ret, ok +} + +func (s *Wrapper) ConfigOrDefault(sublabel, dflt string) (string, bool) { + if val, ok := s.Config(sublabel); ok { + return val, true + } + return dflt, false +} + +func (s *Wrapper) ConfigCSV(sublabel string, dflt []string) ([]string, bool) { + if val, ok := s.Config(sublabel); ok { + return strings.Split(val, ","), true + } + return dflt, false +} + +func (s *Wrapper) ConfigInt(sublabel string, dflt int) (int, bool) { + val, ok := s.Config(sublabel) + if !ok { + return dflt, false + } + + if ival, err := strconv.Atoi(val); err != nil { + logrus.Warnf("Unable to parse %s on %s: %v. Using default of %d", sublabel, s.NameID(), err, dflt) + return dflt, false + } else { + return ival, true + } +} + +func (s *Wrapper) ConfigDuration(sublabel string, dflt time.Duration) (time.Duration, bool) { + val, ok := s.Config(sublabel) + if !ok { + return dflt, false + } + + if dur, err := time.ParseDuration(val); err != nil { + logrus.Warnf("Unable to parse %s on %s: %v. Using default of %s", sublabel, s.NameID(), err, dflt.String()) + return dflt, false + } else { + return dur, true + } +} + +// true if state is running +func (s *Wrapper) IsRunning() bool { + return s.State == "running" +} + +// Wrap a container set +func wrapContainers(cts ...types.Container) []Wrapper { + ret := make([]Wrapper, len(cts)) + for i, c := range cts { + ret[i] = Wrapper{c} + } + sort.Slice(ret, func(i, j int) bool { + return ret[i].NameID() < ret[j].NameID() + }) + return ret +} + +func wrapListResult(cts []types.Container, err error) ([]Wrapper, error) { + return wrapContainers(cts...), err +} diff --git a/pkg/service/container.go b/pkg/service/container.go index 990db37..5092de1 100644 --- a/pkg/service/container.go +++ b/pkg/service/container.go @@ -1,12 +1,9 @@ package service import ( - "sort" - "strings" "time" "traefik-lazyload/pkg/config" - - "github.com/docker/docker/api/types" + "traefik-lazyload/pkg/containers" ) type containerSettings struct { @@ -26,21 +23,21 @@ type ContainerState struct { pinned bool // Don't remove, even if not started } -func newStateFromContainer(ct *types.Container) *ContainerState { +func newStateFromContainer(ct *containers.Wrapper) *ContainerState { return &ContainerState{ - name: containerShort(ct), + name: ct.NameID(), containerSettings: extractContainerLabels(ct), lastActivity: time.Now(), started: time.Now(), } } -func extractContainerLabels(ct *types.Container) (target containerSettings) { - target.stopDelay, _ = labelOrDefaultDuration(ct, "stopdelay", config.Model.StopDelay) - target.waitForCode, _ = labelOrDefaultInt(ct, "waitforcode", 200) - target.waitForPath, _ = labelOrDefault(ct, "waitforpath", "/") - target.waitForMethod, _ = labelOrDefault(ct, "waitformethod", "HEAD") - target.needs, _ = labelOrDefaultArr(ct, "needs") +func extractContainerLabels(ct *containers.Wrapper) (target containerSettings) { + target.stopDelay, _ = ct.ConfigDuration("stopdelay", config.Model.StopDelay) + target.waitForCode, _ = ct.ConfigInt("waitforcode", 200) + target.waitForPath, _ = ct.ConfigOrDefault("waitforpath", "/") + target.waitForMethod, _ = ct.ConfigOrDefault("waitformethod", "HEAD") + target.needs, _ = ct.ConfigCSV("needs", nil) return } @@ -52,7 +49,7 @@ func (s *ContainerState) LastActive() time.Time { return s.lastActivity } -func (s *ContainerState) LastActiveAge() string { +func (s *ContainerState) LastActiveAge() string { // FIXME: Return duration (update UI) return time.Since(s.lastActivity).Round(time.Second).String() } @@ -68,7 +65,7 @@ func (s *ContainerState) Started() time.Time { return s.started } -func (s *containerSettings) StopDelay() string { +func (s *containerSettings) StopDelay() string { // FIXME: Return duration (update UI) return s.stopDelay.String() } @@ -83,35 +80,3 @@ func (s *ContainerState) WaitForPath() string { func (s *ContainerState) WaitForMethod() string { return s.waitForMethod } - -// Wrapper for container results that opaques and adds some methods to that data -type ContainerWrapper struct { - types.Container -} - -func (s *ContainerWrapper) NameID() string { - return containerShort(&s.Container) -} - -func (s *ContainerWrapper) ConfigLabels() map[string]string { - var matchString = config.Model.LabelPrefix + "." - - ret := make(map[string]string) - for k, v := range s.Labels { - if strings.HasPrefix(k, matchString) { - ret[k[len(matchString):]] = v - } - } - return ret -} - -func wrapContainers(cts ...types.Container) []ContainerWrapper { - ret := make([]ContainerWrapper, len(cts)) - for i, c := range cts { - ret[i] = ContainerWrapper{c} - } - sort.Slice(ret, func(i, j int) bool { - return ret[i].NameID() < ret[j].NameID() - }) - return ret -} diff --git a/pkg/service/labels.go b/pkg/service/labels.go deleted file mode 100644 index 5270e39..0000000 --- a/pkg/service/labels.go +++ /dev/null @@ -1,53 +0,0 @@ -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 - } -} diff --git a/pkg/service/service.go b/pkg/service/service.go index 3b7bf47..b05bb47 100644 --- a/pkg/service/service.go +++ b/pkg/service/service.go @@ -5,14 +5,12 @@ import ( "encoding/json" "errors" "sort" - "strings" "sync" "time" - "traefik-lazyload/pkg/config" + "traefik-lazyload/pkg/containers" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" - "github.com/docker/docker/api/types/filters" "github.com/docker/docker/client" "github.com/sirupsen/logrus" ) @@ -21,12 +19,13 @@ type Core struct { mux sync.Mutex term chan bool - client *client.Client + client *client.Client + discovery *containers.Discovery active map[string]*ContainerState // cid -> state } -func New(client *client.Client, pollRate time.Duration) (*Core, error) { +func New(client *client.Client, discovery *containers.Discovery, pollRate time.Duration) (*Core, error) { // Test client and report if info, err := client.Info(context.Background()); err != nil { return nil, err @@ -36,9 +35,10 @@ func New(client *client.Client, pollRate time.Duration) (*Core, error) { // Make core ret := &Core{ - client: client, - active: make(map[string]*ContainerState), - term: make(chan bool), + client: client, + discovery: discovery, + active: make(map[string]*ContainerState), + term: make(chan bool), } ret.Poll() // initial force-poll to update @@ -61,7 +61,7 @@ func (s *Core) StartHost(hostname string) (*ContainerState, error) { ctx := context.Background() - ct, err := s.findContainerByHostname(ctx, hostname) + ct, err := s.discovery.FindContainerByHostname(ctx, hostname) if err != nil { logrus.Warnf("Unable to find container for host %s: %s", hostname, err) return nil, err @@ -85,7 +85,7 @@ func (s *Core) StartHost(hostname string) (*ContainerState, error) { ets.lastActivity = time.Now() s.mux.Unlock() }() - s.startDependencyFor(ctx, ets.needs, containerShort(ct)) + s.startDependencyFor(ctx, ets.needs, ct.NameID()) s.startContainerSync(ctx, ct) }() @@ -107,26 +107,38 @@ func (s *Core) StopAll() { } } -func (s *Core) startContainerSync(ctx context.Context, ct *types.Container) error { - if isRunning(ct) { - return nil - } - +// Returns all actively managed containers +func (s *Core) ActiveContainers() []*ContainerState { s.mux.Lock() defer s.mux.Unlock() + ret := make([]*ContainerState, 0, len(s.active)) + for _, item := range s.active { + ret = append(ret, item) + } + sort.Slice(ret, func(i, j int) bool { + return ret[i].name < ret[j].name + }) + return ret +} + +func (s *Core) startContainerSync(ctx context.Context, ct *containers.Wrapper) error { + if ct.IsRunning() { + return nil + } + if err := s.client.ContainerStart(ctx, ct.ID, types.ContainerStartOptions{}); err != nil { - logrus.Warnf("Error starting container %s: %s", containerShort(ct), err) + logrus.Warnf("Error starting container %s: %s", ct.NameID(), err) return err } else { - logrus.Infof("Started container %s", containerShort(ct)) + logrus.Infof("Started container %s", ct.NameID()) } return nil } func (s *Core) startDependencyFor(ctx context.Context, needs []string, forContainer string) { for _, dep := range needs { - providers, err := s.findContainersByDepProvider(ctx, dep) + providers, err := s.discovery.FindDepProvider(ctx, dep) if err != nil { logrus.Errorf("Error finding dependency provider for %s: %v", dep, err) @@ -134,12 +146,12 @@ func (s *Core) startDependencyFor(ctx context.Context, needs []string, forContai logrus.Warnf("Unable to find any container that provides %s for %s", dep, forContainer) } else { for _, provider := range providers { - if !isRunning(&provider) { - logrus.Infof("Starting dependency for %s: %s", forContainer, containerShort(&provider)) + if !provider.IsRunning() { + logrus.Infof("Starting dependency for %s: %s", forContainer, provider.NameID()) s.startContainerSync(ctx, &provider) - delay, _ := labelOrDefaultDuration(&provider, "provides.delay", 2*time.Second) + delay, _ := provider.ConfigDuration("provides.delay", 2*time.Second) logrus.Debugf("Delaying %s to start %s", delay.String(), dep) time.Sleep(delay) } @@ -167,15 +179,15 @@ func (s *Core) stopDependenciesFor(ctx context.Context, cid string, cts *Contain for dep, needed := range deps { if !needed { logrus.Infof("Stopping dependency %s...", dep) - containers, err := s.findContainersByDepProvider(ctx, dep) + containers, err := s.discovery.FindDepProvider(ctx, dep) if err != nil { logrus.Errorf("Unable to find dependency provider containers for %s: %v", dep, err) } else if len(containers) == 0 { logrus.Warnf("Unable to find any containers for dependency %s", dep) } else { for _, ct := range containers { - if isRunning(&ct) { - logrus.Infof("Stopping %s...", containerShort(&ct)) + if ct.IsRunning() { + logrus.Infof("Stopping %s...", ct.NameID()) go s.client.ContainerStop(ctx, ct.ID, container.StopOptions{}) } } @@ -215,16 +227,16 @@ func (s *Core) Poll() { } func (s *Core) checkForNewContainersSync(ctx context.Context) { - containers, err := s.findAllLazyloadContainers(ctx, false) + cts, err := s.discovery.FindAllLazyload(ctx, false) if err != nil { logrus.Warnf("Error checking for new containers: %v", err) return } - runningContainers := make(map[string]*types.Container) - for i, ct := range containers { - if isRunning(&ct) { - runningContainers[ct.ID] = &containers[i] + runningContainers := make(map[string]*containers.Wrapper) + for i, ct := range cts { + if ct.IsRunning() { + runningContainers[ct.ID] = &cts[i] } } @@ -240,7 +252,7 @@ func (s *Core) checkForNewContainersSync(ctx context.Context) { // now, look for containers that are running, but aren't in our active inventory for _, ct := range runningContainers { if _, ok := s.active[ct.ID]; !ok { - logrus.Infof("Discovered running container %s", containerShort(ct)) + logrus.Infof("Discovered running container %s", ct.NameID()) s.active[ct.ID] = newStateFromContainer(ct) } } @@ -306,88 +318,3 @@ func (s *Core) checkContainerForInactivity(ctx context.Context, cid string, ct * return false, nil } - -func (s *Core) findContainersByDepProvider(ctx context.Context, name string) ([]types.Container, error) { - filters := filters.NewArgs() - filters.Add("label", config.SubLabel("provides")+"="+name) - return s.client.ContainerList(ctx, types.ContainerListOptions{ - Filters: filters, - All: true, - }) -} - -func (s *Core) findContainerByHostname(ctx context.Context, hostname string) (*types.Container, error) { - containers, err := s.findAllLazyloadContainers(ctx, true) - if err != nil { - return nil, err - } - - for _, c := range containers { - if hostStr, ok := labelOrDefault(&c, "hosts", ""); ok { - hosts := strings.Split(hostStr, ",") - if strSliceContains(hosts, hostname) { - return &c, nil - } - } else { - // If not defined explicitely, infer from traefik route - for k, v := range c.Labels { - if strings.Contains(k, "traefik.http.routers.") && strings.Contains(v, hostname) { // TODO: More complex - return &c, nil - } - } - } - } - - return nil, ErrNotFound -} - -// Finds all containers on node that are labeled with lazyloader config -func (s *Core) findAllLazyloadContainers(ctx context.Context, includeStopped bool) ([]types.Container, error) { - filters := filters.NewArgs() - filters.Add("label", config.Model.LabelPrefix) - - return s.client.ContainerList(ctx, types.ContainerListOptions{ - All: includeStopped, - Filters: filters, - }) -} - -// Returns all actively managed containers -func (s *Core) ActiveContainers() []*ContainerState { - s.mux.Lock() - defer s.mux.Unlock() - - ret := make([]*ContainerState, 0, len(s.active)) - for _, item := range s.active { - ret = append(ret, item) - } - sort.Slice(ret, func(i, j int) bool { - return ret[i].name < ret[j].name - }) - return ret -} - -// Return all containers that qualify to be load-managed (eg. have the tag) -func (s *Core) QualifyingContainers(ctx context.Context) []ContainerWrapper { - ct, err := s.findAllLazyloadContainers(ctx, true) - if err != nil { - return nil - } - - return wrapContainers(ct...) -} - -func (s *Core) ProviderContainers(ctx context.Context) []ContainerWrapper { - filters := filters.NewArgs() - filters.Add("label", config.SubLabel("provides")) - - ct, err := s.client.ContainerList(ctx, types.ContainerListOptions{ - Filters: filters, - All: true, - }) - if err != nil { - return nil - } - - return wrapContainers(ct...) -} diff --git a/pkg/service/util.go b/pkg/service/util.go index b2fb2d5..4706019 100644 --- a/pkg/service/util.go +++ b/pkg/service/util.go @@ -1,9 +1,6 @@ package service import ( - "fmt" - "strings" - "github.com/docker/docker/api/types" ) @@ -14,38 +11,3 @@ func sumNetworkBytes(networks map[string]types.NetworkStats) (recv int64, send i } return } - -func shortId(id string) string { - const SLEN = 8 - if len(id) <= SLEN { - return id - } - return id[:SLEN] -} - -func containerShort(c *types.Container) string { - var name string - if len(c.Names) > 0 { - name = trimRootPath(c.Names[0]) - } else { - name = c.Image - } - return fmt.Sprintf("%s(%s)", name, shortId(c.ID)) -} - -func trimRootPath(s string) string { - return strings.TrimPrefix(s, "/") -} - -func isRunning(c *types.Container) bool { - return c.State == "running" -} - -func strSliceContains(slice []string, s string) bool { - for _, item := range slice { - if item == s { - return true - } - } - return false -}