package cluster import ( "io/fs" "path" "sort" "strings" "darvaza.org/core" "git.jpi.io/amery/jpictl/pkg/rings" ) const ( // ZoneRegionsFileName indicates the file containing // region names as references ZoneRegionsFileName = "regions" // RegionClusterTokenFileName contains the kubernetes // token of the cluster this region represents RegionClusterTokenFileName = "k8s_token" ) func (m *Cluster) scan(opts *ScanOptions) error { for _, fn := range []func(*ScanOptions) error{ m.scanDirectory, m.scanMachines, m.initRegions, m.scanZoneIDs, m.scanSort, m.scanGateways, m.scanCephMonitors, } { if err := fn(opts); err != nil { return err } } return nil } func (m *Cluster) scanDirectory(opts *ScanOptions) error { // each directory is a zone entries, err := fs.ReadDir(m.dir, ".") if err != nil { return err } for _, e := range entries { if e.IsDir() { ok, err := m.scanSubdirectory(opts, e.Name()) switch { case err != nil: return core.Wrap(err, e.Name()) case !ok: m.warn(nil). WithField("zone", e.Name()). Print("empty") } } } return nil } func (m *Cluster) scanSubdirectory(_ *ScanOptions, name string) (bool, error) { z, err := m.newZone(name) switch { case err != nil: // somewhere went wrong scanning the subdirectory return false, err case z.Machines.Len() > 0: // zones have machines and the regions they belong m.Zones = append(m.Zones, z) return true, nil case len(z.Regions) > 0: // regions have no machines but can include // other regions m.appendRegionRegions(name, z.Regions...) return true, nil default: // empty return false, nil } } func (m *Cluster) newZone(name string) (*Zone, error) { z := &Zone{ zones: m, logger: m, Name: name, } z.debug(). WithField("zone", z.Name). Print("found") if err := z.scan(); err != nil { return nil, err } return z, nil } func (m *Cluster) scanMachines(opts *ScanOptions) error { var err error m.ForEachMachine(func(p *Machine) bool { err = p.scan(opts) return err != nil }) m.ForEachMachine(func(p *Machine) bool { err = p.scanWrapUp(opts) return err != nil }) return err } func (m *Cluster) scanZoneIDs(_ *ScanOptions) error { var hasMissing bool var lastZoneID rings.ZoneID m.ForEachZone(func(z *Zone) bool { switch { case z.ID == 0: hasMissing = true case z.ID > lastZoneID: lastZoneID = z.ID } return false }) if hasMissing { next := lastZoneID + 1 m.ForEachZone(func(z *Zone) bool { if z.ID == 0 { z.ID, next = next, next+1 } return false }) } return nil } func (m *Cluster) scanSort(_ *ScanOptions) error { sort.SliceStable(m.Zones, func(i, j int) bool { id1 := m.Zones[i].ID id2 := m.Zones[j].ID return id1 < id2 }) m.ForEachZone(func(z *Zone) bool { sort.Sort(z) return false }) m.ForEachMachine(func(p *Machine) bool { sort.SliceStable(p.Rings, func(i, j int) bool { ri1 := p.Rings[i] ri2 := p.Rings[j] return ri1.Ring < ri2.Ring }) return false }) return nil } func (m *Cluster) scanGateways(_ *ScanOptions) error { var err error m.ForEachZone(func(z *Zone) bool { _, _, err = z.GetGateway() return err != nil }) return err } func (z *Zone) scan() error { // each directory is a machine entries, err := fs.ReadDir(z.zones.dir, z.Name) if err != nil { return err } for _, e := range entries { name := e.Name() switch { case name == ZoneRegionsFileName: err = z.loadRegions() case name == RegionClusterTokenFileName: err = z.loadClusterToken() case e.IsDir(): err = z.scanSubdirectory(name) default: z.warn(nil). WithField("zone", z.Name). WithField("filename", name). Print("unknown") } if err != nil { return err } } return nil } func (z *Zone) loadRegions() error { filename := path.Join(z.Name, ZoneRegionsFileName) regions, err := z.zones.ReadLines(filename) if err == nil { // parsed err = z.appendRegions(regions...) if err != nil { err = core.Wrap(err, filename) } } return err } func (z *Zone) loadClusterToken() error { var token string filename := path.Join(z.Name, RegionClusterTokenFileName) lines, err := z.zones.ReadLines(filename) if err != nil { return err } // first non-empty line for _, s := range lines { s = strings.TrimSpace(s) if s != "" { token = s break } } err = z.zones.setRegionClusterToken(z.Name, token) if err != nil { err = core.Wrap(err, filename) } return err } func (z *Zone) scanSubdirectory(name string) error { m := &Machine{ zone: z, logger: z, Name: name, } m.debug(). WithField("node", m.Name). WithField("zone", z.Name). Print("found") if err := m.init(); err != nil { m.error(err). WithField("node", m.Name). WithField("zone", z.Name). Print() return err } z.Machines = append(z.Machines, m) return nil } // GetGateway returns the first gateway found, if none // files will be created to enable the first [Machine] to // be one func (z *Zone) GetGateway() (*Machine, bool, error) { var first *Machine var gateway *Machine z.zones.ForEachMachine(func(p *Machine) bool { switch { case p.IsGateway(): // found gateway = p case first == nil: // remember first = p default: // keep looking } return gateway != nil }) switch { case gateway != nil: // found one return gateway, false, nil case first != nil: // make one if err := first.SetGateway(true); err != nil { return first, false, err } return first, true, nil default: // Zone without nodes? panic("unreachable") } }